add file
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$result = $conn->query("SELECT * FROM spbu");
|
||||
|
||||
$data = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_POST['id'];
|
||||
$nama = $_POST['nama'];
|
||||
$wa = $_POST['wa'];
|
||||
$status = $_POST['status'];
|
||||
|
||||
$conn->query("UPDATE spbu SET
|
||||
nama_spbu='$nama',
|
||||
no_wa='$wa',
|
||||
status='$status'
|
||||
WHERE id=$id");
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$conn->query("DELETE FROM spbu WHERE id=$id");
|
||||
?>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base target="_top">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Quick Start - Leaflet</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
height: 400px;
|
||||
width: 600px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
<div id="map" style="width: 600px; height: 400px;"></div>
|
||||
<script>
|
||||
|
||||
const map = L.map('map').setView([-0.0553, 109.3495], 13);
|
||||
|
||||
const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
const marker = L.marker([-0.0553, 109.3495]).addTo(map)
|
||||
.bindPopup('<b>Hello world!</b><br />I am a popup.').openPopup();
|
||||
|
||||
const circle = L.circle([-0.056, 109.35], {
|
||||
color: 'red',
|
||||
fillColor: '#f03',
|
||||
fillOpacity: 0.5,
|
||||
radius: 500
|
||||
}).addTo(map).bindPopup('I am a circle.');
|
||||
|
||||
const polygon = L.polygon([
|
||||
[-0.054, 109.34],
|
||||
[-0.057, 109.36],
|
||||
[-0.052, 109.355]
|
||||
]).addTo(map).bindPopup('I am a polygon.');
|
||||
|
||||
|
||||
const popup = L.popup()
|
||||
.setLatLng([51.513, -0.09])
|
||||
.setContent('I am a standalone popup.')
|
||||
.openOn(map);
|
||||
|
||||
function onMapClick(e) {
|
||||
popup
|
||||
.setLatLng(e.latlng)
|
||||
.setContent(`You clicked the map at ${e.latlng.toString()}`)
|
||||
.openOn(map);
|
||||
}
|
||||
|
||||
map.on('click', onMapClick);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GIS SPBU PRO FINAL</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
|
||||
<style>
|
||||
#map { height: 100vh; }
|
||||
|
||||
.btn {
|
||||
padding: 4px 8px;
|
||||
margin-top: 5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit { background: orange; color: white; }
|
||||
.delete { background: red; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script>
|
||||
const map = L.map('map').setView([-0.02, 109.34], 13);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
let markers = [];
|
||||
|
||||
// ICON
|
||||
const greenIcon = new L.Icon({
|
||||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
|
||||
iconSize: [32, 32]
|
||||
});
|
||||
|
||||
const redIcon = new L.Icon({
|
||||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
|
||||
iconSize: [32, 32]
|
||||
});
|
||||
|
||||
// ================= LOAD DATA =================
|
||||
function loadData() {
|
||||
markers.forEach(m => map.removeLayer(m));
|
||||
markers = [];
|
||||
|
||||
fetch('ambil.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(d => {
|
||||
|
||||
let icon = d.status === 'yes' ? greenIcon : redIcon;
|
||||
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: icon,
|
||||
draggable: true
|
||||
}).addTo(map);
|
||||
|
||||
// DRAG SIMPAN LOKASI (FIX)
|
||||
marker.on('dragend', function(e){
|
||||
const pos = e.target.getLatLng();
|
||||
|
||||
const dataSend = new URLSearchParams();
|
||||
dataSend.append("id", d.id);
|
||||
dataSend.append("lat", pos.lat);
|
||||
dataSend.append("lng", pos.lng);
|
||||
|
||||
fetch('update_lokasi.php', {
|
||||
method: 'POST',
|
||||
body: dataSend
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
alert("Lokasi berhasil diupdate!");
|
||||
});
|
||||
});
|
||||
|
||||
marker.bindPopup(`
|
||||
<b>${d.nama_spbu}</b><br>
|
||||
WA: ${d.no_wa}<br>
|
||||
24 Jam: <span style="color:${d.status === 'yes' ? 'green' : 'red'}">${d.status}</span><br>
|
||||
|
||||
<button class="btn edit" onclick="editData(${d.id}, '${d.nama_spbu}', '${d.no_wa}', '${d.status}')">Edit</button>
|
||||
<button class="btn delete" onclick="hapusData(${d.id})">Hapus</button>
|
||||
`);
|
||||
|
||||
markers.push(marker);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ================= EDIT =================
|
||||
function editData(id, nama, wa, status) {
|
||||
const form = `
|
||||
<form id="editForm">
|
||||
Nama:<br>
|
||||
<input type="text" name="nama" value="${nama}"><br>
|
||||
WA:<br>
|
||||
<input type="text" name="wa" value="${wa}"><br>
|
||||
Status:<br>
|
||||
<select name="status">
|
||||
<option ${status=='yes'?'selected':''} value="yes">Yes</option>
|
||||
<option ${status=='no'?'selected':''} value="no">No</option>
|
||||
</select><br><br>
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
L.popup().setLatLng(map.getCenter()).setContent(form).openOn(map);
|
||||
|
||||
setTimeout(()=>{
|
||||
document.getElementById('editForm').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
const data = new FormData(this);
|
||||
data.append('id', id);
|
||||
|
||||
fetch('edit.php', {
|
||||
method:'POST',
|
||||
body:data
|
||||
}).then(()=>{
|
||||
alert("Data diupdate!");
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
},300);
|
||||
}
|
||||
|
||||
// ================= HAPUS =================
|
||||
function hapusData(id) {
|
||||
if(confirm("Yakin hapus?")) {
|
||||
fetch('hapus.php?id=' + id)
|
||||
.then(()=>{
|
||||
alert("Data dihapus!");
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ================= TAMBAH =================
|
||||
map.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
const form = `
|
||||
<form id="formInput">
|
||||
Nama SPBU:<br>
|
||||
<input type="text" name="nama_spbu" required><br>
|
||||
|
||||
No WA:<br>
|
||||
<input type="text" name="no_wa" required><br>
|
||||
|
||||
Status:<br>
|
||||
<select name="status">
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No</option>
|
||||
</select><br><br>
|
||||
|
||||
<input type="hidden" name="latitude" value="${lat}">
|
||||
<input type="hidden" name="longitude" value="${lng}">
|
||||
|
||||
<button type="submit">Simpan</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
L.popup().setLatLng(e.latlng).setContent(form).openOn(map);
|
||||
|
||||
setTimeout(()=>{
|
||||
document.getElementById('formInput').addEventListener('submit', function(ev){
|
||||
ev.preventDefault();
|
||||
|
||||
const data = new FormData(this);
|
||||
|
||||
fetch('simpan.php',{
|
||||
method:'POST',
|
||||
body:data
|
||||
}).then(()=>{
|
||||
alert("Tersimpan!");
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
},300);
|
||||
});
|
||||
|
||||
loadData();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST');
|
||||
$user = getenv('DB_USER');
|
||||
$pass = getenv('DB_PASSWORD');
|
||||
$db = getenv('DB_NAME');
|
||||
|
||||
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$nama = $_POST['nama_spbu'];
|
||||
$wa = $_POST['no_wa'];
|
||||
$status = $_POST['status'];
|
||||
$lat = $_POST['latitude'];
|
||||
$lng = $_POST['longitude'];
|
||||
|
||||
$sql = "INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude)
|
||||
VALUES ('$nama', '$wa', '$status', '$lat', '$lng')";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo "success";
|
||||
} else {
|
||||
echo "error";
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_POST['id'];
|
||||
$lat = $_POST['lat'];
|
||||
$lng = $_POST['lng'];
|
||||
|
||||
$sql = "UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id='$id'";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo "success";
|
||||
} else {
|
||||
echo "error: " . $conn->error;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
@@ -0,0 +1,54 @@
|
||||
-- ============================================================
|
||||
-- DATABASE PROYEK 02 — Jalanan, Parsil, Jalan Rusak
|
||||
-- Import ini ke phpMyAdmin (dari server, bukan dari database)
|
||||
-- ============================================================
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- Database data_jalan: tabel jalan & parsil
|
||||
CREATE DATABASE IF NOT EXISTS data_jalan
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE data_jalan;
|
||||
|
||||
DROP TABLE IF EXISTS parsil;
|
||||
DROP TABLE IF EXISTS jalan;
|
||||
|
||||
CREATE TABLE jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
status ENUM('baik','sedang','rusak_ringan','rusak_berat') DEFAULT 'baik',
|
||||
panjang DECIMAL(10,2) DEFAULT 0,
|
||||
geometry LONGTEXT COMMENT 'GeoJSON LineString',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE parsil (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
pemilik VARCHAR(150) NOT NULL,
|
||||
status ENUM('milik_sendiri','sewa','hibah','sengketa') DEFAULT 'milik_sendiri',
|
||||
luas DECIMAL(12,2) DEFAULT 0,
|
||||
geometry LONGTEXT COMMENT 'GeoJSON Polygon',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Database webgis: tabel jalan_rusak
|
||||
CREATE DATABASE IF NOT EXISTS webgis
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis;
|
||||
|
||||
DROP TABLE IF EXISTS jalan_rusak;
|
||||
|
||||
CREATE TABLE jalan_rusak (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
keterangan TEXT,
|
||||
tingkat ENUM('ringan','sedang','berat') DEFAULT 'sedang',
|
||||
geometry LONGTEXT COMMENT 'GeoJSON geometry',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
-- ============================================================
|
||||
-- SELESAI — 2 database, 3 tabel siap dipakai
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST');
|
||||
$user = getenv('DB_USER');
|
||||
$pass = getenv('DB_PASSWORD');
|
||||
$db = getenv('DB_NAME');
|
||||
|
||||
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($conn->query("DELETE FROM jalan WHERE id=$id")) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($conn->query("DELETE FROM parsil WHERE id=$id")) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($conn->query("DELETE FROM jalan_rusak WHERE id=$id")) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$result = $conn->query("SELECT * FROM jalan");
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$result = $conn->query("SELECT * FROM parsil");
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$row['geometry'] = json_decode($row['geometry']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$q = $conn->query("SELECT * FROM jalan_rusak");
|
||||
$data = [];
|
||||
while ($d = $q->fetch_assoc()) {
|
||||
$d['geometry'] = json_decode($d['geometry']);
|
||||
$data[] = $d;
|
||||
}
|
||||
echo json_encode($data);
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WEB GIS FINAL</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
|
||||
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-geometryutil"></script>
|
||||
|
||||
<style>
|
||||
#map { height: 100vh; }
|
||||
|
||||
#searchBox{
|
||||
position:absolute;
|
||||
top:10px;
|
||||
left:260px;
|
||||
z-index:999;
|
||||
padding:8px;
|
||||
width:250px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display:none;
|
||||
position:fixed;
|
||||
z-index:999;
|
||||
left:0;top:0;
|
||||
width:100%;height:100%;
|
||||
background:rgba(0,0,0,0.5);
|
||||
}
|
||||
.modal-content{
|
||||
background:white;
|
||||
padding:20px;
|
||||
width:300px;
|
||||
margin:120px auto;
|
||||
border-radius:10px;
|
||||
text-align:center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<input type="text" id="searchBox" placeholder="Cari semua data...">
|
||||
<div id="map"></div>
|
||||
|
||||
<div id="formModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 id="formTitle"></h3>
|
||||
<input type="text" id="input1"><br>
|
||||
<select id="input2"></select><br>
|
||||
<button onclick="submitForm()">Simpan</button>
|
||||
<button onclick="closeForm()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ================= MAP =================
|
||||
var map = L.map('map').setView([-0.05,109.34],13);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
||||
|
||||
var drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
let semuaLayer = [];
|
||||
let currentLayer = null;
|
||||
let currentType = null;
|
||||
|
||||
// ================= WARNA =================
|
||||
function warnaJalan(s){
|
||||
return s=="Nasional"?"red":s=="Provinsi"?"blue":"green";
|
||||
}
|
||||
|
||||
function warnaParsil(s){
|
||||
return s=="SHM"?"yellow":
|
||||
s=="HGB"?"orange":
|
||||
s=="HGU"?"brown":
|
||||
"purple";
|
||||
}
|
||||
|
||||
// ================= FORMAT =================
|
||||
function formatMeter(m){ return m.toFixed(2)+" m"; }
|
||||
function formatMeter2(m){ return m.toFixed(2)+" m²"; }
|
||||
|
||||
// ================= FORM =================
|
||||
function openForm(type, layer){
|
||||
currentLayer = layer;
|
||||
currentType = type;
|
||||
|
||||
document.getElementById("formModal").style.display="block";
|
||||
|
||||
let select = document.getElementById("input2");
|
||||
select.innerHTML="";
|
||||
|
||||
if(type=="jalan"){
|
||||
formTitle.innerText="Input Jalan";
|
||||
input1.placeholder="Nama Jalan";
|
||||
|
||||
["Nasional","Provinsi","Kabupaten"].forEach(v=>{
|
||||
let o=document.createElement("option");
|
||||
o.value=v;o.text=v;
|
||||
select.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
else if(type=="parsil"){
|
||||
formTitle.innerText="Input Parsil";
|
||||
input1.placeholder="Nama Pemilik";
|
||||
|
||||
["SHM","HGB","HGU","HP"].forEach(v=>{
|
||||
let o=document.createElement("option");
|
||||
o.value=v;o.text=v;
|
||||
select.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
else if(type=="rusak"){
|
||||
formTitle.innerText="Jalan Rusak";
|
||||
input1.placeholder="Keterangan";
|
||||
|
||||
["Ringan","Sedang","Berat"].forEach(v=>{
|
||||
let o=document.createElement("option");
|
||||
o.value=v;o.text=v;
|
||||
select.appendChild(o);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeForm(){
|
||||
document.getElementById("formModal").style.display="none";
|
||||
input1.value = "";
|
||||
currentLayer = null;
|
||||
currentType = null;
|
||||
// Re-enable draw control
|
||||
map.addControl(drawControl);
|
||||
}
|
||||
|
||||
function submitForm(){
|
||||
let v1 = input1.value.trim();
|
||||
let v2 = input2.value;
|
||||
|
||||
if(!v1){
|
||||
alert("Mohon isi nama/keterangan terlebih dahulu!");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!currentLayer || !currentType){
|
||||
alert("Tidak ada layer yang dipilih!");
|
||||
closeForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// Simpan referensi layer sebelum closeForm me-reset currentLayer
|
||||
let layer = currentLayer;
|
||||
let type = currentType;
|
||||
|
||||
closeForm();
|
||||
|
||||
if(type=="jalan") prosesJalan(layer, v1, v2);
|
||||
else if(type=="parsil") prosesParsil(layer, v1, v2);
|
||||
else if(type=="rusak") prosesRusak(layer, v1, v2);
|
||||
}
|
||||
|
||||
// ================= DRAW =================
|
||||
var drawControl = new L.Control.Draw({
|
||||
edit:{ featureGroup: drawnItems },
|
||||
draw:{
|
||||
polyline:true,
|
||||
polygon:true,
|
||||
rectangle:false,
|
||||
circle:false,
|
||||
marker:true
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
map.on('draw:created', function(e){
|
||||
var layer = e.layer;
|
||||
drawnItems.addLayer(layer);
|
||||
|
||||
// Disable draw sementara sampai form disubmit
|
||||
map.removeControl(drawControl);
|
||||
|
||||
if(e.layerType=="polyline") openForm("jalan",layer);
|
||||
if(e.layerType=="polygon") openForm("parsil",layer);
|
||||
if(e.layerType=="marker") openForm("rusak",layer);
|
||||
});
|
||||
|
||||
// ================= JALAN =================
|
||||
function prosesJalan(layer,nama,status){
|
||||
|
||||
let latlngs = layer.getLatLngs();
|
||||
let panjang=0;
|
||||
for(let i=0;i<latlngs.length-1;i++){
|
||||
panjang += latlngs[i].distanceTo(latlngs[i+1]);
|
||||
}
|
||||
|
||||
layer.setStyle({color:warnaJalan(status)});
|
||||
|
||||
fetch('save_jalan.php',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({nama,status,panjang,geometry:layer.toGeoJSON().geometry})
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(res=> { if(res.success) layer.id=res.id; else console.error('save_jalan error:',res.error); });
|
||||
|
||||
layer.bindPopup(`<b>Jalan</b><br>${nama}<br>Panjang: ${formatMeter(panjang)}`);
|
||||
|
||||
semuaLayer.push({layer:layer,nama:nama});
|
||||
}
|
||||
|
||||
// ================= PARSIL =================
|
||||
function prosesParsil(layer,pemilik,status){
|
||||
|
||||
let luas = L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]);
|
||||
|
||||
layer.setStyle({color:warnaParsil(status)});
|
||||
|
||||
fetch('save_parsil.php',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({pemilik,status,luas,geometry:layer.toGeoJSON().geometry})
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(res=> { if(res.success) layer.id=res.id; else console.error('save_parsil error:',res.error); });
|
||||
|
||||
layer.bindPopup(`<b>Parsil</b><br>${pemilik}<br>Luas: ${formatMeter2(luas)}`);
|
||||
|
||||
semuaLayer.push({layer:layer,nama:pemilik});
|
||||
}
|
||||
|
||||
// ================= RUSAK =================
|
||||
function prosesRusak(layer,keterangan,tingkat){
|
||||
|
||||
let icon = L.icon({
|
||||
iconUrl: tingkat=="Ringan"?
|
||||
"https://maps.google.com/mapfiles/ms/icons/green-dot.png":
|
||||
tingkat=="Sedang"?
|
||||
"https://maps.google.com/mapfiles/ms/icons/yellow-dot.png":
|
||||
"https://maps.google.com/mapfiles/ms/icons/red-dot.png",
|
||||
iconSize:[25,41],
|
||||
iconAnchor:[12,41]
|
||||
});
|
||||
|
||||
layer.setIcon(icon);
|
||||
|
||||
fetch('save_rusak.php',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({keterangan,tingkat,geometry:layer.toGeoJSON().geometry})
|
||||
})
|
||||
.then(r=>r.json())
|
||||
.then(res=> { if(res.success) layer.id=res.id; else console.error('save_rusak error:',res.error); });
|
||||
|
||||
layer.bindPopup(`<b>Rusak</b><br>${keterangan}<br><b>Tingkat:</b> ${tingkat}`);
|
||||
|
||||
semuaLayer.push({layer:layer,nama:keterangan});
|
||||
}
|
||||
|
||||
// ================= LOAD =================
|
||||
fetch('get_jalan.php').then(r=>r.json()).then(data=>{
|
||||
data.forEach(j=>{
|
||||
let geo=j.geometry;
|
||||
let layer=L.geoJSON(geo,{style:{color:warnaJalan(j.status)}}).addTo(map);
|
||||
layer.eachLayer(l=>{
|
||||
l.bindPopup(`<b>Jalan</b><br>${j.nama}<br>Panjang: ${formatMeter(j.panjang)}`);
|
||||
semuaLayer.push({layer:l,nama:j.nama});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetch('get_parsil.php').then(r=>r.json()).then(data=>{
|
||||
data.forEach(p=>{
|
||||
let geo=p.geometry;
|
||||
let layer=L.geoJSON(geo,{style:{color:warnaParsil(p.status)}}).addTo(map);
|
||||
layer.eachLayer(l=>{
|
||||
l.bindPopup(`<b>Parsil</b><br>${p.pemilik}<br>Luas: ${formatMeter2(p.luas)}`);
|
||||
semuaLayer.push({layer:l,nama:p.pemilik});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetch('get_rusak.php').then(r=>r.json()).then(data=>{
|
||||
data.forEach(r=>{
|
||||
let geo=r.geometry;
|
||||
|
||||
let icon = L.icon({
|
||||
iconUrl: r.tingkat=="Ringan"?
|
||||
"https://maps.google.com/mapfiles/ms/icons/green-dot.png":
|
||||
r.tingkat=="Sedang"?
|
||||
"https://maps.google.com/mapfiles/ms/icons/yellow-dot.png":
|
||||
"https://maps.google.com/mapfiles/ms/icons/red-dot.png",
|
||||
iconSize:[25,41],
|
||||
iconAnchor:[12,41]
|
||||
});
|
||||
|
||||
let layer=L.geoJSON(geo,{
|
||||
pointToLayer:(f,latlng)=>L.marker(latlng,{icon:icon})
|
||||
}).addTo(map);
|
||||
|
||||
layer.eachLayer(l=>{
|
||||
l.bindPopup(`<b>Rusak</b><br>${r.keterangan}<br><b>Tingkat:</b> ${r.tingkat}`);
|
||||
semuaLayer.push({layer:l,nama:r.keterangan});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ================= SEARCH =================
|
||||
document.getElementById("searchBox").addEventListener("keyup",function(){
|
||||
|
||||
let key=this.value.toLowerCase();
|
||||
|
||||
semuaLayer.forEach(o=>{
|
||||
|
||||
let cocok=o.nama.toLowerCase().includes(key);
|
||||
|
||||
if(o.layer.setStyle){
|
||||
o.layer.setStyle({
|
||||
opacity:cocok?1:0.1,
|
||||
fillOpacity:cocok?0.7:0.1
|
||||
});
|
||||
}
|
||||
|
||||
if(o.layer.setOpacity){
|
||||
o.layer.setOpacity(cocok?1:0.2);
|
||||
}
|
||||
|
||||
if(cocok){
|
||||
try{ map.fitBounds(o.layer.getBounds()); }catch(e){}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST');
|
||||
$user = getenv('DB_USER');
|
||||
$pass = getenv('DB_PASSWORD');
|
||||
$db = getenv('DB_NAME');
|
||||
|
||||
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(['success' => false, 'error' => 'Data tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$nama = $conn->real_escape_string($data['nama'] ?? '');
|
||||
$status = $conn->real_escape_string($data['status'] ?? 'baik');
|
||||
$panjang = floatval($data['panjang'] ?? 0);
|
||||
$geometry = $conn->real_escape_string(json_encode($data['geometry']));
|
||||
|
||||
$sql = "INSERT INTO jalan (nama, status, panjang, geometry)
|
||||
VALUES ('$nama', '$status', '$panjang', '$geometry')";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo json_encode(['success' => true, 'id' => $conn->insert_id]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(['success' => false, 'error' => 'Data tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pemilik = $conn->real_escape_string($data['pemilik'] ?? '');
|
||||
$status = $conn->real_escape_string($data['status'] ?? 'milik_sendiri');
|
||||
$luas = floatval($data['luas'] ?? 0);
|
||||
$geometry = $conn->real_escape_string(json_encode($data['geometry']));
|
||||
|
||||
$sql = "INSERT INTO parsil (pemilik, status, luas, geometry)
|
||||
VALUES ('$pemilik', '$status', '$luas', '$geometry')";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo json_encode(['success' => true, 'id' => $conn->insert_id]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(['success' => false, 'error' => 'Data tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$keterangan = $conn->real_escape_string($data->keterangan ?? '');
|
||||
$tingkat = $conn->real_escape_string($data->tingkat ?? 'sedang');
|
||||
$geometry = $conn->real_escape_string(json_encode($data->geometry));
|
||||
|
||||
$sql = "INSERT INTO jalan_rusak (keterangan, tingkat, geometry)
|
||||
VALUES ('$keterangan', '$tingkat', '$geometry')";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo json_encode(['success' => true, 'id' => $conn->insert_id]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$panjang = floatval($data['panjang'] ?? 0);
|
||||
$geometry = $conn->real_escape_string(json_encode($data['geometry']));
|
||||
|
||||
if ($conn->query("UPDATE jalan SET panjang='$panjang', geometry='$geometry' WHERE id=$id")) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$luas = floatval($data['luas'] ?? 0);
|
||||
$geometry = $conn->real_escape_string(json_encode($data['geometry']));
|
||||
|
||||
if ($conn->query("UPDATE parsil SET luas='$luas', geometry='$geometry' WHERE id=$id")) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $conn->error]);
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<base target="_top">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WebGIS Choropleth — Kota Pontianak</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--accent: #58a6ff;
|
||||
--text: #e6edf3;
|
||||
--muted: #8b949e;
|
||||
--c1: #fff7bc;
|
||||
--c2: #fec44f;
|
||||
--c3: #fb9a29;
|
||||
--c4: #ec7014;
|
||||
--c5: #cc4c02;
|
||||
--c6: #8c2d04;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Space Mono', monospace;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── HEADER ─────────────────────────────────────── */
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 14px 24px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 1000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-dot {
|
||||
width: 10px; height: 10px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 12px var(--accent);
|
||||
animation: pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity:1; transform:scale(1); }
|
||||
50% { opacity:.5; transform:scale(1.4); }
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .05em;
|
||||
color: var(--text);
|
||||
}
|
||||
header span.sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-left: 4px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.mode-btns {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.mode-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
.mode-btn.active,
|
||||
.mode-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: rgba(88,166,255,.08);
|
||||
}
|
||||
|
||||
/* ── MAIN ────────────────────────────────────────── */
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── SIDEBAR ─────────────────────────────────────── */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-section h3 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Legend */
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.legend-swatch {
|
||||
width: 24px; height: 14px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.legend-label { color: var(--text); }
|
||||
.legend-sub { color: var(--muted); font-size: 10px; }
|
||||
|
||||
/* Stat cards */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.stat-card:hover, .stat-card.active {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.stat-name {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.stat-value {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
.stat-unit { font-size: 10px; color: var(--muted); font-weight: 400; }
|
||||
|
||||
/* Detail panel */
|
||||
#detail-panel {
|
||||
display: none;
|
||||
}
|
||||
#detail-panel.visible {
|
||||
display: block;
|
||||
}
|
||||
.detail-name {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
margin-bottom: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
}
|
||||
.detail-row:last-child { border-bottom: none; }
|
||||
.detail-key { color: var(--muted); }
|
||||
.detail-val { color: var(--text); font-weight: 700; }
|
||||
.density-bar {
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.density-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--c2), var(--c5));
|
||||
transition: width .6s ease;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
#placeholder em {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
/* ── MAP ─────────────────────────────────────────── */
|
||||
#map {
|
||||
flex: 1;
|
||||
background: #0a0f16;
|
||||
}
|
||||
|
||||
/* Override Leaflet UI for dark mode */
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--surface) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: var(--border) !important;
|
||||
}
|
||||
.leaflet-bar { border: 1px solid var(--border) !important; }
|
||||
|
||||
/* ── INFO TOOLTIP ────────────────────────────────── */
|
||||
.info-box {
|
||||
background: var(--surface) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text) !important;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.4) !important;
|
||||
min-width: 160px;
|
||||
}
|
||||
.info-box strong {
|
||||
display: block;
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 6px;
|
||||
color: var(--accent);
|
||||
}
|
||||
.info-box span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ── TOOLTIP POPUPS ─────────────────────────────── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--surface) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.6) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--surface) !important; }
|
||||
.leaflet-popup-content {
|
||||
color: var(--text) !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
font-size: 12px !important;
|
||||
margin: 12px 16px !important;
|
||||
}
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo-dot"></div>
|
||||
<div>
|
||||
<h1>WebGIS Pontianak <span class="sub">// Choropleth</span></h1>
|
||||
</div>
|
||||
<div class="mode-btns">
|
||||
<button class="mode-btn active" onclick="setMode('density',this)">Kepadatan</button>
|
||||
<button class="mode-btn" onclick="setMode('penduduk',this)">Penduduk</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="sidebar-section" id="legend-wrap">
|
||||
<h3>Legenda</h3>
|
||||
<div id="legend-items"></div>
|
||||
</div>
|
||||
|
||||
<!-- Kecamatan Cards -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Kecamatan</h3>
|
||||
<div class="stat-grid" id="stat-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Detail -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Detail</h3>
|
||||
<div id="detail-panel">
|
||||
<div class="detail-name" id="d-name"></div>
|
||||
<div class="detail-row"><span class="detail-key">Penduduk</span><span class="detail-val" id="d-pop"></span></div>
|
||||
<div class="detail-row"><span class="detail-key">Luas Wilayah</span><span class="detail-val" id="d-area"></span></div>
|
||||
<div class="detail-row"><span class="detail-key">Kepadatan</span><span class="detail-val" id="d-den"></span></div>
|
||||
<div class="density-bar"><div class="density-fill" id="d-bar"></div></div>
|
||||
</div>
|
||||
<div id="placeholder">
|
||||
<em>⬤</em>
|
||||
Klik area di peta untuk melihat data kecamatan secara detail.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- MAP -->
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<script src="pontianak.js"></script>
|
||||
<script>
|
||||
// ── CONFIG ────────────────────────────────────────────────────────────────────
|
||||
const MODES = {
|
||||
density: {
|
||||
key: 'density',
|
||||
label: 'Kepadatan Penduduk',
|
||||
unit: 'jiwa/km²',
|
||||
breaks: [2000, 5000, 7000, 10000, 12000],
|
||||
colors: ['#fff7bc','#fec44f','#fb9a29','#ec7014','#cc4c02','#8c2d04'],
|
||||
fmt: v => v.toLocaleString('id-ID') + ' jiwa/km²',
|
||||
},
|
||||
penduduk: {
|
||||
key: 'penduduk',
|
||||
label: 'Jumlah Penduduk',
|
||||
unit: 'jiwa',
|
||||
breaks: [75000, 85000, 95000, 105000, 115000],
|
||||
colors: ['#c6dbef','#9ecae1','#6baed6','#3182bd','#2171b5','#084594'],
|
||||
fmt: v => v.toLocaleString('id-ID') + ' jiwa',
|
||||
}
|
||||
};
|
||||
|
||||
let currentMode = 'density';
|
||||
let geojsonLayer, lastSelected;
|
||||
|
||||
// ── MAP INIT ──────────────────────────────────────────────────────────────────
|
||||
const map = L.map('map', { zoomControl: true }).setView([-0.02, 109.34], 12);
|
||||
|
||||
// Dark OSM tiles
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap contributors © CARTO',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── HELPERS ───────────────────────────────────────────────────────────────────
|
||||
function getColor(value, mode) {
|
||||
const m = MODES[mode];
|
||||
for (let i = 0; i < m.breaks.length; i++) {
|
||||
if (value <= m.breaks[i]) return m.colors[i];
|
||||
}
|
||||
return m.colors[m.colors.length - 1];
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
const val = feature.properties[MODES[currentMode].key];
|
||||
return {
|
||||
fillColor: getColor(val, currentMode),
|
||||
fillOpacity: 0.72,
|
||||
color: '#0d1117',
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function highlightStyle(feature) {
|
||||
return {
|
||||
fillOpacity: 0.9,
|
||||
color: '#58a6ff',
|
||||
weight: 2.5,
|
||||
};
|
||||
}
|
||||
|
||||
// ── SIDEBAR UPDATERS ──────────────────────────────────────────────────────────
|
||||
function buildLegend() {
|
||||
const m = MODES[currentMode];
|
||||
const container = document.getElementById('legend-items');
|
||||
container.innerHTML = '';
|
||||
const labels = [
|
||||
`< ${m.breaks[0].toLocaleString('id-ID')}`,
|
||||
...m.breaks.slice(0,-1).map((v,i) => `${v.toLocaleString('id-ID')} – ${m.breaks[i+1].toLocaleString('id-ID')}`),
|
||||
`> ${m.breaks[m.breaks.length-1].toLocaleString('id-ID')}`,
|
||||
];
|
||||
m.colors.forEach((c,i) => {
|
||||
container.innerHTML += `
|
||||
<div class="legend-item">
|
||||
<div class="legend-swatch" style="background:${c}"></div>
|
||||
<span class="legend-label">${labels[i]} <span class="legend-sub">${m.unit}</span></span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function buildCards() {
|
||||
const grid = document.getElementById('stat-grid');
|
||||
grid.innerHTML = '';
|
||||
const m = MODES[currentMode];
|
||||
pontianakData.features.forEach(f => {
|
||||
const p = f.properties;
|
||||
const val = p[m.key];
|
||||
grid.innerHTML += `
|
||||
<div class="stat-card" id="card-${slugify(p.Ket)}" onclick="selectByName('${p.Ket}')">
|
||||
<div class="stat-name">${p.Ket.replace('Pontianak ','')}</div>
|
||||
<div class="stat-value">${formatShort(val)} <span class="stat-unit">${m.unit.split('/')[0]}</span></div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function slugify(s) { return s.replace(/\s+/g,'_'); }
|
||||
|
||||
function formatShort(v) {
|
||||
if (v >= 1000) return (v/1000).toFixed(1) + 'k';
|
||||
return v.toLocaleString('id-ID');
|
||||
}
|
||||
|
||||
function showDetail(props) {
|
||||
document.getElementById('detail-panel').classList.add('visible');
|
||||
document.getElementById('placeholder').style.display = 'none';
|
||||
document.getElementById('d-name').textContent = props.Ket;
|
||||
document.getElementById('d-pop').textContent = props.penduduk.toLocaleString('id-ID') + ' jiwa';
|
||||
document.getElementById('d-area').textContent = props.luas_km2.toFixed(2) + ' km²';
|
||||
document.getElementById('d-den').textContent = props.density.toLocaleString('id-ID') + ' jiwa/km²';
|
||||
|
||||
// Bar fill based on density max (~12000)
|
||||
const pct = Math.min(100, (props.density / 13000) * 100);
|
||||
document.getElementById('d-bar').style.width = pct + '%';
|
||||
|
||||
// Highlight card
|
||||
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
|
||||
const card = document.getElementById('card-' + slugify(props.Ket));
|
||||
if (card) card.classList.add('active');
|
||||
}
|
||||
|
||||
// ── INTERACTION ───────────────────────────────────────────────────────────────
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({
|
||||
mouseover(e) {
|
||||
const l = e.target;
|
||||
l.setStyle(highlightStyle(feature));
|
||||
l.bringToFront();
|
||||
infoBox.update(feature.properties);
|
||||
},
|
||||
mouseout(e) {
|
||||
if (lastSelected !== e.target) {
|
||||
geojsonLayer.resetStyle(e.target);
|
||||
}
|
||||
infoBox.update(null);
|
||||
},
|
||||
click(e) {
|
||||
map.fitBounds(e.target.getBounds(), { padding: [40, 40] });
|
||||
if (lastSelected && lastSelected !== e.target) {
|
||||
geojsonLayer.resetStyle(lastSelected);
|
||||
}
|
||||
e.target.setStyle(highlightStyle(feature));
|
||||
e.target.bringToFront();
|
||||
lastSelected = e.target;
|
||||
showDetail(feature.properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectByName(name) {
|
||||
geojsonLayer.eachLayer(l => {
|
||||
if (l.feature.properties.Ket === name) {
|
||||
map.fitBounds(l.getBounds(), { padding: [40, 40] });
|
||||
if (lastSelected && lastSelected !== l) geojsonLayer.resetStyle(lastSelected);
|
||||
l.setStyle(highlightStyle(l.feature));
|
||||
l.bringToFront();
|
||||
lastSelected = l;
|
||||
showDetail(l.feature.properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── INFO BOX ──────────────────────────────────────────────────────────────────
|
||||
const infoBox = L.control({ position: 'topright' });
|
||||
infoBox.onAdd = function() {
|
||||
this._div = L.DomUtil.create('div', 'info-box');
|
||||
this.update(null);
|
||||
return this._div;
|
||||
};
|
||||
infoBox.update = function(props) {
|
||||
if (!props) {
|
||||
this._div.innerHTML = '<strong>Peta Pontianak</strong><span>Arahkan kursor ke area</span>';
|
||||
return;
|
||||
}
|
||||
const m = MODES[currentMode];
|
||||
const val = props[m.key];
|
||||
this._div.innerHTML = `
|
||||
<strong>${props.Ket}</strong>
|
||||
<span>${m.label}</span>
|
||||
<span style="color:#e6edf3;font-size:14px;font-weight:700;margin-top:2px;display:block">
|
||||
${val.toLocaleString('id-ID')} <small style="color:#8b949e;font-size:10px">${m.unit}</small>
|
||||
</span>`;
|
||||
};
|
||||
infoBox.addTo(map);
|
||||
|
||||
// ── RENDER ─────────────────────────────────────────────────────────────────────
|
||||
function renderMap() {
|
||||
if (geojsonLayer) map.removeLayer(geojsonLayer);
|
||||
lastSelected = null;
|
||||
|
||||
geojsonLayer = L.geoJson(pontianakData, {
|
||||
style,
|
||||
onEachFeature,
|
||||
}).addTo(map);
|
||||
|
||||
buildLegend();
|
||||
buildCards();
|
||||
|
||||
// Reset detail
|
||||
document.getElementById('detail-panel').classList.remove('visible');
|
||||
document.getElementById('placeholder').style.display = '';
|
||||
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
|
||||
}
|
||||
|
||||
function setMode(mode, btn) {
|
||||
currentMode = mode;
|
||||
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
renderMap();
|
||||
if (lastSelected) infoBox.update(null);
|
||||
}
|
||||
|
||||
// Init
|
||||
renderMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$result = $conn->query("SELECT * FROM spbu");
|
||||
|
||||
$data = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_POST['id'];
|
||||
$nama = $_POST['nama'];
|
||||
$wa = $_POST['wa'];
|
||||
$status = $_POST['status'];
|
||||
|
||||
$conn->query("UPDATE spbu SET
|
||||
nama_spbu='$nama',
|
||||
no_wa='$wa',
|
||||
status='$status'
|
||||
WHERE id=$id");
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$conn->query("DELETE FROM spbu WHERE id=$id");
|
||||
?>
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GIS SPBU PRO FINAL</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
|
||||
<style>
|
||||
#map{
|
||||
height:100vh;
|
||||
}
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
font-family:'Segoe UI',sans-serif;
|
||||
}
|
||||
|
||||
/* tombol popup marker */
|
||||
.btn{
|
||||
padding:8px 14px;
|
||||
border:none;
|
||||
border-radius:10px;
|
||||
cursor:pointer;
|
||||
font-size:14px;
|
||||
font-weight:700;
|
||||
margin-top:8px;
|
||||
margin-right:6px;
|
||||
transition:.2s;
|
||||
}
|
||||
|
||||
.btn:hover{
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
|
||||
.edit{
|
||||
background:#f59e0b;
|
||||
color:white;
|
||||
}
|
||||
|
||||
.delete{
|
||||
background:#ef4444;
|
||||
color:white;
|
||||
}
|
||||
|
||||
/* popup utama */
|
||||
.leaflet-popup-content-wrapper{
|
||||
border-radius:24px;
|
||||
box-shadow:0 14px 35px rgba(0,0,0,.18);
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
/* ukuran NORMAL semua popup */
|
||||
.leaflet-popup-content{
|
||||
margin:0 !important;
|
||||
min-width:300px;
|
||||
max-width:360px;
|
||||
padding:22px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip{
|
||||
width:18px;
|
||||
height:18px;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
/* close */
|
||||
.leaflet-popup-close-button{
|
||||
top:12px !important;
|
||||
right:14px !important;
|
||||
font-size:28px !important;
|
||||
color:#888 !important;
|
||||
}
|
||||
|
||||
/* form popup */
|
||||
.leaflet-popup-content form{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:12px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content label{
|
||||
font-size:15px;
|
||||
font-weight:700;
|
||||
color:#333;
|
||||
margin-bottom:-4px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content input,
|
||||
.leaflet-popup-content select{
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
padding:12px 14px;
|
||||
border:1px solid #ddd;
|
||||
border-radius:14px;
|
||||
font-size:15px;
|
||||
outline:none;
|
||||
background:#fff;
|
||||
}
|
||||
|
||||
.leaflet-popup-content input:focus,
|
||||
.leaflet-popup-content select:focus{
|
||||
border-color:#ff8a00;
|
||||
box-shadow:0 0 0 4px rgba(255,138,0,.12);
|
||||
}
|
||||
|
||||
.leaflet-popup-content button[type="submit"]{
|
||||
margin-top:8px;
|
||||
padding:13px;
|
||||
border:none;
|
||||
border-radius:16px;
|
||||
background:linear-gradient(90deg,#ff9800,#ff6d00);
|
||||
color:white;
|
||||
font-size:17px;
|
||||
font-weight:800;
|
||||
cursor:pointer;
|
||||
transition:.2s;
|
||||
}
|
||||
|
||||
.leaflet-popup-content button[type="submit"]:hover{
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
|
||||
/* popup marker info biar rapi */
|
||||
.leaflet-popup-content b{
|
||||
font-size:18px;
|
||||
color:#111;
|
||||
}
|
||||
|
||||
.leaflet-popup-content span{
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
/* layer control checkbox TIDAK DIUBAH */
|
||||
.leaflet-control-layers{
|
||||
border-radius:18px !important;
|
||||
box-shadow:0 8px 22px rgba(0,0,0,.15);
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded{
|
||||
padding:12px;
|
||||
font-size:15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script>
|
||||
const map = L.map('map').setView([-0.02, 109.34], 13);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ================= LAYER GROUPS =================
|
||||
const layer24Jam = L.layerGroup().addTo(map);
|
||||
const layerNon24Jam = L.layerGroup().addTo(map);
|
||||
|
||||
// Overlay layers untuk Layers Control
|
||||
const overlayLayers = {
|
||||
"🟢 SPBU Buka 24 Jam" : layer24Jam,
|
||||
"🔴 SPBU Tidak Buka 24 Jam" : layerNon24Jam
|
||||
};
|
||||
|
||||
// Tambahkan Layers Control ke peta
|
||||
L.control.layers(null, overlayLayers, { collapsed: false }).addTo(map);
|
||||
|
||||
// ================= ICON =================
|
||||
const greenIcon = new L.Icon({
|
||||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
|
||||
iconSize: [32, 32]
|
||||
});
|
||||
|
||||
const redIcon = new L.Icon({
|
||||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
|
||||
iconSize: [32, 32]
|
||||
});
|
||||
|
||||
let markerList = [];
|
||||
|
||||
// ================= LOAD DATA =================
|
||||
function loadData() {
|
||||
layer24Jam.clearLayers();
|
||||
layerNon24Jam.clearLayers();
|
||||
markerList = [];
|
||||
|
||||
fetch('ambil.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(d => {
|
||||
|
||||
const is24Jam = d.status === 'yes';
|
||||
const icon = is24Jam ? greenIcon : redIcon;
|
||||
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: icon,
|
||||
draggable: true
|
||||
});
|
||||
|
||||
// Tambahkan ke layer group yang sesuai
|
||||
if (is24Jam) {
|
||||
layer24Jam.addLayer(marker);
|
||||
} else {
|
||||
layerNon24Jam.addLayer(marker);
|
||||
}
|
||||
|
||||
// DRAG SIMPAN LOKASI
|
||||
marker.on('dragend', function(e){
|
||||
const pos = e.target.getLatLng();
|
||||
|
||||
const dataSend = new URLSearchParams();
|
||||
dataSend.append("id", d.id);
|
||||
dataSend.append("lat", pos.lat);
|
||||
dataSend.append("lng", pos.lng);
|
||||
|
||||
fetch('update_lokasi.php', {
|
||||
method: 'POST',
|
||||
body: dataSend
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
alert("Lokasi berhasil diupdate!");
|
||||
});
|
||||
});
|
||||
|
||||
marker.bindPopup(`
|
||||
<b>${d.nama_spbu}</b><br>
|
||||
WA: ${d.no_wa}<br>
|
||||
24 Jam: <span style="color:${is24Jam ? 'green' : 'red'}">${is24Jam ? 'Ya' : 'Tidak'}</span><br>
|
||||
|
||||
<button class="btn edit" onclick="editData(${d.id}, '${d.nama_spbu}', '${d.no_wa}', '${d.status}')">Edit</button>
|
||||
<button class="btn delete" onclick="hapusData(${d.id})">Hapus</button>
|
||||
`);
|
||||
|
||||
markerList.push(marker);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ================= EDIT =================
|
||||
function editData(id, nama, wa, status) {
|
||||
const form = `
|
||||
<form id="editForm">
|
||||
Nama:<br>
|
||||
<input type="text" name="nama" value="${nama}"><br>
|
||||
WA:<br>
|
||||
<input type="text" name="wa" value="${wa}"><br>
|
||||
Status:<br>
|
||||
<select name="status">
|
||||
<option ${status=='yes'?'selected':''} value="yes">Yes (Buka 24 Jam)</option>
|
||||
<option ${status=='no'?'selected':''} value="no">No (Tidak 24 Jam)</option>
|
||||
</select><br><br>
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
L.popup().setLatLng(map.getCenter()).setContent(form).openOn(map);
|
||||
|
||||
setTimeout(()=>{
|
||||
document.getElementById('editForm').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
|
||||
const data = new FormData(this);
|
||||
data.append('id', id);
|
||||
|
||||
fetch('edit.php', {
|
||||
method:'POST',
|
||||
body:data
|
||||
}).then(()=>{
|
||||
alert("Data diupdate!");
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
},300);
|
||||
}
|
||||
|
||||
// ================= HAPUS =================
|
||||
function hapusData(id) {
|
||||
if(confirm("Yakin hapus?")) {
|
||||
fetch('hapus.php?id=' + id)
|
||||
.then(()=>{
|
||||
alert("Data dihapus!");
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ================= TAMBAH =================
|
||||
map.on('click', function(e) {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
const form = `
|
||||
<form id="formInput">
|
||||
Nama SPBU:<br>
|
||||
<input type="text" name="nama_spbu" required><br>
|
||||
|
||||
No WA:<br>
|
||||
<input type="text" name="no_wa" required><br>
|
||||
|
||||
Status:<br>
|
||||
<select name="status">
|
||||
<option value="yes">Yes (Buka 24 Jam)</option>
|
||||
<option value="no">No (Tidak 24 Jam)</option>
|
||||
</select><br><br>
|
||||
|
||||
<input type="hidden" name="latitude" value="${lat}">
|
||||
<input type="hidden" name="longitude" value="${lng}">
|
||||
|
||||
<button type="submit">Simpan</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
L.popup().setLatLng(e.latlng).setContent(form).openOn(map);
|
||||
|
||||
setTimeout(()=>{
|
||||
document.getElementById('formInput').addEventListener('submit', function(ev){
|
||||
ev.preventDefault();
|
||||
|
||||
const data = new FormData(this);
|
||||
|
||||
fetch('simpan.php',{
|
||||
method:'POST',
|
||||
body:data
|
||||
}).then(()=>{
|
||||
alert("Tersimpan!");
|
||||
loadData();
|
||||
});
|
||||
});
|
||||
},300);
|
||||
});
|
||||
|
||||
loadData();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
$host = getenv('DB_HOST');
|
||||
$user = getenv('DB_USER');
|
||||
$pass = getenv('DB_PASSWORD');
|
||||
$db = getenv('DB_NAME');
|
||||
|
||||
$conn = mysqli_connect($host, $user, $pass, $db);
|
||||
|
||||
if (!$conn) {
|
||||
die("Koneksi database gagal: " . mysqli_connect_error());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$nama = $_POST['nama_spbu'];
|
||||
$wa = $_POST['no_wa'];
|
||||
$status = $_POST['status'];
|
||||
$lat = $_POST['latitude'];
|
||||
$lng = $_POST['longitude'];
|
||||
|
||||
$sql = "INSERT INTO spbu (nama_spbu, no_wa, status, latitude, longitude)
|
||||
VALUES ('$nama', '$wa', '$status', '$lat', '$lng')";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo "success";
|
||||
} else {
|
||||
echo "error";
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
|
||||
$id = $_POST['id'];
|
||||
$lat = $_POST['lat'];
|
||||
$lng = $_POST['lng'];
|
||||
|
||||
$sql = "UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id='$id'";
|
||||
|
||||
if ($conn->query($sql)) {
|
||||
echo "success";
|
||||
} else {
|
||||
echo "error: " . $conn->error;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY webgis_uas_FIXED_v2/webgis_uas_FIXED/ /var/www/html/
|
||||
|
||||
RUN mkdir -p /var/www/html/uploads/bukti \
|
||||
&& chown -R www-data:www-data /var/www/html
|
||||
@@ -0,0 +1,77 @@
|
||||
# WebGIS Pengentasan Kemiskinan v2.0
|
||||
**Berbasis Masyarakat & Rumah Ibadah — Kota Pontianak**
|
||||
|
||||
## Teknologi
|
||||
- PHP 7.4+ (Backend)
|
||||
- MySQL 5.7+ / MariaDB (Database)
|
||||
- Leaflet.js 1.9 + MarkerCluster (Peta)
|
||||
- Chart.js 4.4 (Dashboard)
|
||||
|
||||
## Instalasi
|
||||
|
||||
1. **Buat database** dan import `database.sql`:
|
||||
```sql
|
||||
mysql -u root -p < database.sql
|
||||
```
|
||||
|
||||
2. **Edit konfigurasi** di `koneksi.php`:
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', 'password_anda');
|
||||
define('DB_NAME', 'webgis_kemiskinan2');
|
||||
```
|
||||
|
||||
3. **Letakkan folder** di `htdocs/` (XAMPP) atau `www/` (Laragon)
|
||||
|
||||
4. **Pastikan folder `uploads/`** bisa ditulis:
|
||||
```bash
|
||||
chmod 755 uploads/
|
||||
```
|
||||
|
||||
5. **Akses** di browser: `http://localhost/webgis_v2/`
|
||||
|
||||
## Akun Demo (Password semua: `password123`)
|
||||
|
||||
| Username | Role | Akses |
|
||||
|------------|-------------------|-------|
|
||||
| admin | Administrator | Full |
|
||||
| pengurus1 | Pengurus Masjid | CRUD + Verifikasi |
|
||||
| pimpinan | Pimpinan Daerah | Read-only + Dashboard |
|
||||
| warga | Masyarakat | Lihat + Laporan |
|
||||
|
||||
## Struktur File
|
||||
```
|
||||
webgis_v2/
|
||||
├── index.php ← Halaman utama (peta WebGIS)
|
||||
├── login.php ← Login multi-role
|
||||
├── logout.php
|
||||
├── dashboard.php ← Dashboard analitik + grafik
|
||||
├── keluarga.php ← Manajemen KK
|
||||
├── riwayat_pendidikan.php ← Riwayat pendidikan & pelatihan
|
||||
├── koneksi.php ← Config + helper functions
|
||||
├── database.sql ← Schema + data dummy
|
||||
├── api/
|
||||
│ ├── penduduk.php ← API CRUD penduduk
|
||||
│ ├── keluarga.php ← API CRUD keluarga
|
||||
│ ├── laporan.php ← API laporan masyarakat
|
||||
│ ├── bantuan.php ← API histori bantuan
|
||||
│ ├── pesan.php ← API chat pengurus
|
||||
│ ├── rumah_ibadah.php ← API rumah ibadah
|
||||
│ ├── riwayat_pendidikan.php
|
||||
│ ├── notifikasi.php
|
||||
│ └── log.php
|
||||
└── uploads/ ← Foto penduduk, laporan, bukti bantuan
|
||||
```
|
||||
|
||||
## Fitur Utama
|
||||
- 🗺️ **WebGIS Leaflet** — marker warna merah/kuning/hijau, cluster, radius rumah ibadah
|
||||
- 👥 **CRUD Penduduk** — NIK, umur otomatis, validasi pendidikan vs usia
|
||||
- 🏠 **Manajemen KK** — relasi keluarga, auto-detect rumah ibadah terdekat
|
||||
- 📢 **Laporan Masyarakat** — publik, anonim, tracking status, urgensi darurat
|
||||
- 🎁 **Tracking Bantuan** — histori lengkap, cek sudah/belum dibantu
|
||||
- 🔥 **Prioritas Otomatis** — deteksi miskin + sakit = DARURAT
|
||||
- 📊 **Dashboard Analitik** — grafik Chart.js, sebaran per kecamatan
|
||||
- 💬 **Chat Pengurus** — komunikasi antar rumah ibadah
|
||||
- 🔐 **Multi-Role** — Admin / Pengurus / Pimpinan / Masyarakat
|
||||
- 📝 **Log Aktivitas** — semua perubahan data tercatat
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
// api/bantuan.php — Histori Bantuan & Jenis Bantuan
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
// ============================================================
|
||||
// LIST histori bantuan
|
||||
// ============================================================
|
||||
if ($action === 'list') {
|
||||
$penduduk_id = (int)($_GET['penduduk_id'] ?? 0);
|
||||
$where = $penduduk_id ? "WHERE hb.penduduk_id=$penduduk_id" : '';
|
||||
|
||||
$sql = "SELECT hb.*,
|
||||
p.nama AS nama_penerima, p.nik,
|
||||
jb.nama AS nama_bantuan, jb.kategori,
|
||||
pb.nama_program,
|
||||
u.nama AS nama_penyalur,
|
||||
ri.nama AS nama_ibadah
|
||||
FROM histori_bantuan hb
|
||||
JOIN penduduk p ON hb.penduduk_id = p.id
|
||||
JOIN jenis_bantuan jb ON hb.jenis_bantuan_id = jb.id
|
||||
LEFT JOIN program_bantuan pb ON hb.program_bantuan_id = pb.id
|
||||
LEFT JOIN users u ON hb.disalurkan_oleh = u.id
|
||||
LEFT JOIN rumah_ibadah ri ON hb.rumah_ibadah_id = ri.id
|
||||
$where
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
LIMIT 200";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// JENIS BANTUAN
|
||||
// ============================================================
|
||||
if ($action === 'jenis') {
|
||||
$rows = $conn->query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori, nama");
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROGRAM BANTUAN
|
||||
// ============================================================
|
||||
if ($action === 'program') {
|
||||
$rows = $conn->query("SELECT pb.*, jb.nama AS nama_jenis, u.nama AS nama_pj
|
||||
FROM program_bantuan pb
|
||||
JOIN jenis_bantuan jb ON pb.jenis_bantuan_id = jb.id
|
||||
LEFT JOIN users u ON pb.penanggungjawab = u.id
|
||||
ORDER BY pb.created_at DESC");
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CEK RIWAYAT — apakah sudah pernah dibantu jenis tertentu
|
||||
// ============================================================
|
||||
if ($action === 'cek') {
|
||||
$penduduk_id = (int)($_GET['penduduk_id'] ?? 0);
|
||||
$jenis_id = (int)($_GET['jenis_bantuan_id'] ?? 0);
|
||||
if (!$penduduk_id) jsonError('penduduk_id wajib');
|
||||
|
||||
$where = "WHERE hb.penduduk_id=$penduduk_id AND hb.status_penyaluran='disalurkan'";
|
||||
if ($jenis_id) $where .= " AND hb.jenis_bantuan_id=$jenis_id";
|
||||
|
||||
$rows = $conn->query("SELECT hb.*, jb.nama AS nama_bantuan FROM histori_bantuan hb JOIN jenis_bantuan jb ON hb.jenis_bantuan_id=jb.id $where ORDER BY hb.tanggal_penyaluran DESC");
|
||||
$data = $rows ? $rows->fetch_all(MYSQLI_ASSOC) : [];
|
||||
jsonSuccess(['pernah_dibantu' => count($data) > 0, 'riwayat' => $data, 'total' => count($data)]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SAVE — catat penyaluran bantuan
|
||||
// ============================================================
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin', 'pengurus');
|
||||
|
||||
$penduduk_id = (int)($_POST['penduduk_id'] ?? 0);
|
||||
$keluarga_id = (int)($_POST['keluarga_id'] ?? 0) ?: 'NULL';
|
||||
$program_id = (int)($_POST['program_bantuan_id'] ?? 0) ?: 'NULL';
|
||||
$jenis_id = (int)($_POST['jenis_bantuan_id'] ?? 0);
|
||||
$jumlah = (float)($_POST['jumlah'] ?? 1);
|
||||
$nilai = (float)($_POST['nilai_rupiah'] ?? 0);
|
||||
$tgl = sanitize($conn, $_POST['tanggal_penyaluran'] ?? date('Y-m-d'));
|
||||
$status = sanitize($conn, $_POST['status_penyaluran'] ?? 'disalurkan');
|
||||
$catatan = sanitize($conn, $_POST['catatan'] ?? '');
|
||||
|
||||
if (!$penduduk_id) jsonError('penduduk_id wajib');
|
||||
if (!$jenis_id) jsonError('jenis_bantuan_id wajib');
|
||||
|
||||
// Ambil satuan dari jenis bantuan
|
||||
$jb = dbFirst($conn, "SELECT satuan, nama FROM jenis_bantuan WHERE id=?", [$jenis_id], 'i');
|
||||
$satuan = $jb ? sanitize($conn, $jb['satuan']) : 'paket';
|
||||
|
||||
// Auto set rumah ibadah dari keluarga
|
||||
$ibadah_id = 'NULL';
|
||||
$uid = currentUserId();
|
||||
$userIbadah = dbFirst($conn, "SELECT rumah_ibadah_id FROM users WHERE id=?", [$uid], 'i');
|
||||
if ($userIbadah && $userIbadah['rumah_ibadah_id']) {
|
||||
$ibadah_id = (int)$userIbadah['rumah_ibadah_id'];
|
||||
}
|
||||
|
||||
// Upload bukti
|
||||
$bukti = 'NULL';
|
||||
if (!empty($_FILES['bukti_foto']['name'])) {
|
||||
$f = uploadFile($_FILES['bukti_foto'], 'bantuan', ['jpg','jpeg','png']);
|
||||
if ($f) $bukti = "'$f'";
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO histori_bantuan (penduduk_id, keluarga_id, program_bantuan_id, jenis_bantuan_id, jumlah, satuan, nilai_rupiah, tanggal_penyaluran, status_penyaluran, disalurkan_oleh, rumah_ibadah_id, bukti_foto, catatan)
|
||||
VALUES ($penduduk_id, $keluarga_id, $program_id, $jenis_id, $jumlah, '$satuan', $nilai, '$tgl', '$status', $uid, $ibadah_id, $bukti, '$catatan')";
|
||||
|
||||
if (!$conn->query($sql)) jsonError('Gagal simpan: ' . $conn->error);
|
||||
$newId = $conn->insert_id;
|
||||
|
||||
logAktivitas($conn, 'SALURKAN_BANTUAN', 'histori_bantuan', $newId);
|
||||
jsonSuccess(['id' => $newId], 'Bantuan berhasil dicatat');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// STATISTIK BANTUAN
|
||||
// ============================================================
|
||||
if ($action === 'statistik') {
|
||||
$stat = [];
|
||||
$stat['total_disalurkan'] = $conn->query("SELECT COUNT(*) c FROM histori_bantuan WHERE status_penyaluran='disalurkan'")->fetch_assoc()['c'];
|
||||
$stat['total_penerima'] = $conn->query("SELECT COUNT(DISTINCT penduduk_id) c FROM histori_bantuan WHERE status_penyaluran='disalurkan'")->fetch_assoc()['c'];
|
||||
$stat['total_nilai'] = $conn->query("SELECT SUM(nilai_rupiah) c FROM histori_bantuan WHERE status_penyaluran='disalurkan'")->fetch_assoc()['c'];
|
||||
$stat['per_kategori'] = $conn->query("SELECT jb.kategori, COUNT(*) total, SUM(hb.nilai_rupiah) nilai FROM histori_bantuan hb JOIN jenis_bantuan jb ON hb.jenis_bantuan_id=jb.id WHERE hb.status_penyaluran='disalurkan' GROUP BY jb.kategori")->fetch_all(MYSQLI_ASSOC);
|
||||
$stat['per_ibadah'] = $conn->query("SELECT ri.nama, ri.jenis, COUNT(hb.id) total FROM histori_bantuan hb JOIN rumah_ibadah ri ON hb.rumah_ibadah_id=ri.id WHERE hb.status_penyaluran='disalurkan' GROUP BY ri.id ORDER BY total DESC")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($stat);
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
requireRole('admin','pengurus','pimpinan');
|
||||
$role = getRole();
|
||||
$riId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
$filterRumah = ($role==='pengurus' && $riId) ? "AND jp.rumah_ibadah_id=$riId" : '';
|
||||
$tgl = sanitize($conn, $_GET['tanggal'] ?? date('Y-m-d'));
|
||||
$week = (int)($_GET['week'] ?? 0);
|
||||
$senin = new DateTime('monday this week');
|
||||
$senin->modify("$week weeks");
|
||||
$minggu = clone $senin; $minggu->modify('+6 days');
|
||||
$dari = $senin->format('Y-m-d'); $sampai = $minggu->format('Y-m-d');
|
||||
$rows = $conn->query("
|
||||
SELECT jp.*, u.nama AS nama_pengurus, ri.nama AS nama_ibadah,
|
||||
ug.nama AS nama_pengganti
|
||||
FROM jadwal_piket jp
|
||||
JOIN users u ON jp.user_id=u.id
|
||||
JOIN rumah_ibadah ri ON jp.rumah_ibadah_id=ri.id
|
||||
LEFT JOIN users ug ON jp.digantikan_oleh=ug.id
|
||||
WHERE jp.tanggal BETWEEN '$dari' AND '$sampai' $filterRumah
|
||||
ORDER BY jp.tanggal, FIELD(jp.shift,'pagi','siang','sore','malam','full_day')
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess(['data'=>$rows,'minggu_dari'=>$dari,'minggu_sampai'=>$sampai]);
|
||||
}
|
||||
|
||||
if ($action==='hari_ini') {
|
||||
$role = getRole(); $riId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
$filterRumah = ($role==='pengurus' && $riId) ? "AND jp.rumah_ibadah_id=$riId" : '';
|
||||
$rows = $conn->query("
|
||||
SELECT jp.*, u.nama AS nama_pengurus, ri.nama AS nama_ibadah
|
||||
FROM jadwal_piket jp JOIN users u ON jp.user_id=u.id
|
||||
JOIN rumah_ibadah ri ON jp.rumah_ibadah_id=ri.id
|
||||
WHERE jp.tanggal=CURDATE() AND jp.status IN ('terjadwal','hadir') $filterRumah
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
}
|
||||
|
||||
if ($action==='save' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$uid = (int)($_POST['user_id'] ?? 0);
|
||||
$riId = (int)($_POST['rumah_ibadah_id'] ?? 0);
|
||||
$tgl = sanitize($conn, $_POST['tanggal'] ?? '');
|
||||
$shift = sanitize($conn, $_POST['shift'] ?? 'full_day');
|
||||
$jMul = sanitize($conn, $_POST['jam_mulai'] ?? '08:00');
|
||||
$jSls = sanitize($conn, $_POST['jam_selesai'] ?? '17:00');
|
||||
$ket = sanitize($conn, $_POST['keterangan'] ?? '');
|
||||
if (!$uid || !$riId || !$tgl) jsonError('Data tidak lengkap');
|
||||
$cek = $conn->query("SELECT id FROM jadwal_piket WHERE user_id=$uid AND tanggal='$tgl' AND shift='$shift'")->fetch_assoc();
|
||||
if ($cek) jsonError('Jadwal sudah ada untuk pengurus ini di shift & tanggal yang sama');
|
||||
$conn->query("INSERT INTO jadwal_piket (user_id,rumah_ibadah_id,tanggal,shift,jam_mulai,jam_selesai,keterangan) VALUES ($uid,$riId,'$tgl','$shift','$jMul','$jSls','$ket')");
|
||||
logAktivitas($conn,'CREATE_JADWAL','jadwal_piket',$conn->insert_id);
|
||||
jsonSuccess(['id'=>$conn->insert_id],'Jadwal berhasil ditambahkan');
|
||||
}
|
||||
|
||||
if ($action==='bulk' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$uid = (int)($_POST['user_id'] ?? 0);
|
||||
$riId = (int)($_POST['rumah_ibadah_id'] ?? 0);
|
||||
$dari = sanitize($conn, $_POST['tanggal_mulai'] ?? '');
|
||||
$sampai = sanitize($conn, $_POST['tanggal_selesai'] ?? '');
|
||||
$shift = sanitize($conn, $_POST['shift'] ?? 'full_day');
|
||||
$jMul = sanitize($conn, $_POST['jam_mulai'] ?? '08:00');
|
||||
$jSls = sanitize($conn, $_POST['jam_selesai'] ?? '17:00');
|
||||
$hari = $_POST['hari_aktif'] ?? [];
|
||||
$cur = new DateTime($dari); $end = new DateTime($sampai); $added = 0;
|
||||
while ($cur <= $end) {
|
||||
if (in_array($cur->format('w'), $hari)) {
|
||||
$t = $cur->format('Y-m-d');
|
||||
$cek = $conn->query("SELECT id FROM jadwal_piket WHERE user_id=$uid AND tanggal='$t' AND shift='$shift'")->fetch_assoc();
|
||||
if (!$cek) { $conn->query("INSERT INTO jadwal_piket (user_id,rumah_ibadah_id,tanggal,shift,jam_mulai,jam_selesai) VALUES ($uid,$riId,'$t','$shift','$jMul','$jSls')"); $added++; }
|
||||
}
|
||||
$cur->modify('+1 day');
|
||||
}
|
||||
jsonSuccess(['added'=>$added],"$added jadwal berhasil dibuat");
|
||||
}
|
||||
|
||||
if ($action==='update_status' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status = sanitize($conn, $_POST['status'] ?? '');
|
||||
$gantiId= (int)($_POST['digantikan_oleh'] ?? 0) ?: 'NULL';
|
||||
$conn->query("UPDATE jadwal_piket SET status='$status',digantikan_oleh=$gantiId WHERE id=$id");
|
||||
logAktivitas($conn,'UPDATE_JADWAL','jadwal_piket',$id);
|
||||
jsonSuccess(null,'Status diperbarui');
|
||||
}
|
||||
|
||||
if ($action==='delete' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$conn->query("DELETE FROM jadwal_piket WHERE id=$id");
|
||||
logAktivitas($conn,'DELETE_JADWAL','jadwal_piket',$id);
|
||||
jsonSuccess(null,'Jadwal dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
// api/keluarga.php — CRUD Kartu Keluarga
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
$search = sanitize($conn, $_GET['search'] ?? '');
|
||||
$where = $search ? "WHERE k.no_kk LIKE '%$search%' OR k.alamat LIKE '%$search%'" : '';
|
||||
$sql = "SELECT k.*,
|
||||
ri.nama AS nama_ibadah,
|
||||
(SELECT COUNT(*) FROM penduduk p WHERE p.keluarga_id = k.id) AS jumlah_anggota,
|
||||
(SELECT COUNT(*) FROM penduduk p2 WHERE p2.keluarga_id = k.id AND p2.status_ekonomi = 'miskin') AS jumlah_miskin,
|
||||
u.nama AS nama_verifikator
|
||||
FROM keluarga k
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id = ri.id
|
||||
LEFT JOIN users u ON k.verified_by = u.id
|
||||
$where
|
||||
ORDER BY FIELD(k.status_ekonomi,'miskin','rentan','mampu'), k.no_kk
|
||||
LIMIT 200";
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'get') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$kk = dbFirst($conn, "SELECT k.*, ri.nama AS nama_ibadah FROM keluarga k LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id WHERE k.id=?", [$id], 'i');
|
||||
if (!$kk) jsonError('Tidak ditemukan', 404);
|
||||
$kk['anggota'] = dbQuery($conn, "SELECT id, nik, nama, jenis_kelamin, TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) AS umur, status_keluarga, pekerjaan, status_ekonomi, kondisi_kesehatan FROM penduduk WHERE keluarga_id=? ORDER BY FIELD(status_keluarga,'kepala_keluarga','istri','anak','orang_tua','famili_lain','lainnya')", [$id], 'i');
|
||||
jsonSuccess($kk);
|
||||
}
|
||||
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin', 'pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$no_kk = sanitize($conn, $_POST['no_kk'] ?? '');
|
||||
$alamat = sanitize($conn, $_POST['alamat'] ?? '');
|
||||
$rt = sanitize($conn, $_POST['rt'] ?? '');
|
||||
$rw = sanitize($conn, $_POST['rw'] ?? '');
|
||||
$kel = sanitize($conn, $_POST['kelurahan'] ?? '');
|
||||
$kec = sanitize($conn, $_POST['kecamatan'] ?? '');
|
||||
$kota = sanitize($conn, $_POST['kota'] ?? 'Pontianak');
|
||||
$lat = !empty($_POST['lat']) ? (float)$_POST['lat'] : 'NULL';
|
||||
$lng = !empty($_POST['lng']) ? (float)$_POST['lng'] : 'NULL';
|
||||
$status_ek = sanitize($conn, $_POST['status_ekonomi'] ?? 'rentan');
|
||||
$ibadah_id = (int)($_POST['rumah_ibadah_id'] ?? 0) ?: 'NULL';
|
||||
|
||||
// Auto-detect rumah ibadah terdekat jika lat/lng ada dan ibadah belum dipilih
|
||||
if ($lat !== 'NULL' && $lng !== 'NULL' && $ibadah_id === 'NULL') {
|
||||
$terdekat = cariRumahIbadahTerdekat($conn, (float)$lat, (float)$lng);
|
||||
if ($terdekat) $ibadah_id = $terdekat['id'];
|
||||
}
|
||||
|
||||
if (strlen($no_kk) !== 16) jsonError('No. KK harus 16 digit');
|
||||
if (!$alamat) jsonError('Alamat wajib diisi');
|
||||
|
||||
if ($id > 0) {
|
||||
$conn->query("UPDATE keluarga SET no_kk='$no_kk',alamat='$alamat',rt='$rt',rw='$rw',kelurahan='$kel',kecamatan='$kec',kota='$kota',lat=$lat,lng=$lng,status_ekonomi='$status_ek',rumah_ibadah_id=$ibadah_id WHERE id=$id");
|
||||
logAktivitas($conn, 'UPDATE_KELUARGA', 'keluarga', $id);
|
||||
jsonSuccess(['id'=>$id], 'Data keluarga diperbarui');
|
||||
} else {
|
||||
$dup = $conn->query("SELECT id FROM keluarga WHERE no_kk='$no_kk'")->fetch_assoc();
|
||||
if ($dup) jsonError('No. KK sudah terdaftar');
|
||||
$conn->query("INSERT INTO keluarga (no_kk,alamat,rt,rw,kelurahan,kecamatan,kota,lat,lng,status_ekonomi,rumah_ibadah_id) VALUES ('$no_kk','$alamat','$rt','$rw','$kel','$kec','$kota',$lat,$lng,'$status_ek',$ibadah_id)");
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn, 'CREATE_KELUARGA', 'keluarga', $newId);
|
||||
jsonSuccess(['id'=>$newId], 'Keluarga berhasil ditambahkan');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'verifikasi' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin', 'pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$uid = currentUserId();
|
||||
$conn->query("UPDATE keluarga SET status_verifikasi='terverifikasi', verified_by=$uid, verified_at=NOW() WHERE id=$id");
|
||||
logAktivitas($conn, 'VERIFIKASI_KELUARGA', 'keluarga', $id);
|
||||
jsonSuccess(null, 'Keluarga terverifikasi');
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
// Cek masih ada anggota
|
||||
$anggota = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE keluarga_id=$id")->fetch_assoc()['c'];
|
||||
if ($anggota > 0) jsonError("Masih ada $anggota anggota. Pindahkan anggota terlebih dahulu.");
|
||||
$conn->query("DELETE FROM keluarga WHERE id=$id");
|
||||
jsonSuccess(null, 'Keluarga dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
// api/laporan.php — CRUD Laporan Masyarakat
|
||||
require_once '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
requireLogin();
|
||||
$status = sanitize($conn, $_GET['status'] ?? '');
|
||||
$where = $status ? "WHERE l.status='$status'" : '';
|
||||
$sql = "SELECT l.*, p.nama AS nama_penduduk, u1.nama AS nama_verifikator, u2.nama AS nama_prosesor
|
||||
FROM laporan l
|
||||
LEFT JOIN penduduk p ON l.penduduk_id = p.id
|
||||
LEFT JOIN users u1 ON l.diverifikasi_oleh = u1.id
|
||||
LEFT JOIN users u2 ON l.diproses_oleh = u2.id
|
||||
$where ORDER BY FIELD(l.urgensi,'darurat','tinggi','sedang','rendah'), l.created_at DESC
|
||||
LIMIT 100";
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'map') {
|
||||
$sql = "SELECT id, kode_laporan, nama_pelapor, deskripsi, kategori, urgensi, status, lat, lng
|
||||
FROM laporan WHERE lat IS NOT NULL AND lng IS NOT NULL AND status != 'selesai'";
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Public endpoint — siapapun bisa lapor (tidak perlu login)
|
||||
$nama_pelapor = sanitize($conn, $_POST['nama_pelapor'] ?? '');
|
||||
$no_hp = sanitize($conn, $_POST['no_hp_pelapor'] ?? '');
|
||||
$nama_terlapor = sanitize($conn, $_POST['nama_terlapor'] ?? '');
|
||||
$alamat = sanitize($conn, $_POST['alamat_kejadian'] ?? '');
|
||||
$deskripsi = sanitize($conn, $_POST['deskripsi'] ?? '');
|
||||
$kategori = sanitize($conn, $_POST['kategori'] ?? 'kemiskinan');
|
||||
$urgensi = sanitize($conn, $_POST['urgensi'] ?? 'sedang');
|
||||
$lat = !empty($_POST['lat']) ? (float)$_POST['lat'] : 'NULL';
|
||||
$lng = !empty($_POST['lng']) ? (float)$_POST['lng'] : 'NULL';
|
||||
$penduduk_id = (int)($_POST['penduduk_id'] ?? 0) ?: 'NULL';
|
||||
|
||||
if (!$deskripsi || strlen($deskripsi) < 10) jsonError('Deskripsi terlalu singkat (min 10 karakter)');
|
||||
if (!$alamat) jsonError('Alamat kejadian wajib diisi');
|
||||
|
||||
// Generate kode laporan
|
||||
$thn = date('Y');
|
||||
$last = $conn->query("SELECT COUNT(*) c FROM laporan WHERE YEAR(created_at)=$thn")->fetch_assoc()['c'];
|
||||
$kode = 'LPR-' . $thn . '-' . str_pad($last + 1, 4, '0', STR_PAD_LEFT);
|
||||
|
||||
// Upload foto
|
||||
$foto = 'NULL';
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$f = uploadFile($_FILES['foto'], 'laporan', ['jpg','jpeg','png']);
|
||||
if ($f) $foto = "'$f'";
|
||||
}
|
||||
|
||||
$nama_pelapor_val = $nama_pelapor ? "'$nama_pelapor'" : 'NULL';
|
||||
$no_hp_val = $no_hp ? "'$no_hp'" : 'NULL';
|
||||
$nama_terl_val = $nama_terlapor ? "'$nama_terlapor'" : 'NULL';
|
||||
$alamat_val = $alamat ? "'$alamat'" : 'NULL';
|
||||
|
||||
$sql = "INSERT INTO laporan (kode_laporan, nama_pelapor, no_hp_pelapor, nama_terlapor, penduduk_id, alamat_kejadian, lat, lng, deskripsi, kategori, urgensi, foto)
|
||||
VALUES ('$kode', $nama_pelapor_val, $no_hp_val, $nama_terl_val, $penduduk_id, $alamat_val, $lat, $lng, '$deskripsi', '$kategori', '$urgensi', $foto)";
|
||||
|
||||
if (!$conn->query($sql)) jsonError('Gagal simpan laporan: ' . $conn->error);
|
||||
$newId = $conn->insert_id;
|
||||
|
||||
// Jika urgensi darurat, kirim notifikasi ke semua pengurus
|
||||
if ($urgensi === 'darurat') {
|
||||
$pengurus = $conn->query("SELECT u.id FROM users u JOIN roles r ON u.role_id=r.id WHERE r.slug IN ('admin','pengurus') AND u.aktif=1");
|
||||
while ($p = $pengurus->fetch_assoc()) {
|
||||
kirimNotifikasi($conn, $p['id'], '🚨 Laporan DARURAT Masuk!', "Kode: $kode — $deskripsi", 'danger', '#');
|
||||
}
|
||||
}
|
||||
|
||||
logAktivitas($conn, 'CREATE_LAPORAN', 'laporan', $newId);
|
||||
jsonSuccess(['id' => $newId, 'kode' => $kode], 'Laporan berhasil dikirim. Kode: ' . $kode);
|
||||
}
|
||||
|
||||
if ($action === 'update_status' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireLogin();
|
||||
requireRole('admin', 'pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status = sanitize($conn, $_POST['status'] ?? '');
|
||||
$catatan = sanitize($conn, $_POST['catatan'] ?? '');
|
||||
$uid = currentUserId();
|
||||
|
||||
$allowed = ['diverifikasi', 'diproses', 'selesai', 'ditolak'];
|
||||
if (!in_array($status, $allowed)) jsonError('Status tidak valid');
|
||||
|
||||
$extra = '';
|
||||
if ($status === 'diverifikasi') $extra = ", diverifikasi_oleh=$uid, diverifikasi_at=NOW(), catatan_verifikasi='$catatan'";
|
||||
elseif ($status === 'diproses') $extra = ", diproses_oleh=$uid, diproses_at=NOW()";
|
||||
elseif ($status === 'selesai') $extra = ", diselesaikan_at=NOW()";
|
||||
|
||||
$conn->query("UPDATE laporan SET status='$status'$extra WHERE id=$id");
|
||||
logAktivitas($conn, 'UPDATE_STATUS_LAPORAN', 'laporan', $id);
|
||||
jsonSuccess(null, 'Status laporan diperbarui');
|
||||
}
|
||||
|
||||
|
||||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireLogin();
|
||||
requireRole('admin', 'pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if (!$id) jsonError('ID tidak valid');
|
||||
$old = $conn->query("SELECT * FROM laporan WHERE id=$id")->fetch_assoc();
|
||||
if (!$old) jsonError('Laporan tidak ditemukan');
|
||||
$conn->query("DELETE FROM laporan WHERE id=$id");
|
||||
logAktivitas($conn, 'DELETE_LAPORAN', 'laporan', $id, $old);
|
||||
jsonSuccess(null, 'Laporan berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// api/log.php — Log Aktivitas Sistem
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
requireRole('admin', 'pengurus', 'pimpinan');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
$limit = min((int)($_GET['limit'] ?? 100), 500);
|
||||
$filter = sanitize($conn, $_GET['aksi'] ?? '');
|
||||
$where = $filter ? "WHERE la.aksi LIKE '%$filter%'" : '';
|
||||
|
||||
$sql = "SELECT la.*, u.nama AS nama_user, u.username
|
||||
FROM log_aktivitas la
|
||||
LEFT JOIN users u ON la.user_id = u.id
|
||||
$where
|
||||
ORDER BY la.created_at DESC
|
||||
LIMIT $limit";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'ringkasan') {
|
||||
$stat = [];
|
||||
$stat['total_hari_ini'] = $conn->query("SELECT COUNT(*) c FROM log_aktivitas WHERE DATE(created_at)=CURDATE()")->fetch_assoc()['c'];
|
||||
$stat['per_aksi'] = $conn->query("SELECT aksi, COUNT(*) total FROM log_aktivitas GROUP BY aksi ORDER BY total DESC LIMIT 10")->fetch_all(MYSQLI_ASSOC);
|
||||
$stat['per_user'] = $conn->query("SELECT u.nama, COUNT(la.id) total FROM log_aktivitas la JOIN users u ON la.user_id=u.id GROUP BY la.user_id ORDER BY total DESC LIMIT 10")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($stat);
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// api/notifikasi.php
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$uid = currentUserId();
|
||||
|
||||
if ($action === 'list') {
|
||||
$rows = $conn->query("SELECT * FROM notifikasi WHERE user_id=$uid ORDER BY created_at DESC LIMIT 30");
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'baca' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id) {
|
||||
$conn->query("UPDATE notifikasi SET dibaca=1 WHERE id=$id AND user_id=$uid");
|
||||
} else {
|
||||
$conn->query("UPDATE notifikasi SET dibaca=1 WHERE user_id=$uid");
|
||||
}
|
||||
jsonSuccess(null, 'OK');
|
||||
}
|
||||
|
||||
if ($action === 'count') {
|
||||
$c = $conn->query("SELECT COUNT(*) c FROM notifikasi WHERE user_id=$uid AND dibaca=0")->fetch_assoc()['c'];
|
||||
jsonSuccess(['count' => (int)$c]);
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
// api/penduduk.php — CRUD lengkap untuk penduduk
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
// ============================================================
|
||||
// LIST — untuk tabel panel
|
||||
// ============================================================
|
||||
if ($action === 'list') {
|
||||
$search = sanitize($conn, $_GET['search'] ?? '');
|
||||
$status = sanitize($conn, $_GET['status'] ?? '');
|
||||
|
||||
$where = ["1=1"];
|
||||
if ($search) $where[] = "(p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')";
|
||||
if ($status) $where[] = "p.status_ekonomi = '$status'";
|
||||
$wStr = implode(' AND ', $where);
|
||||
|
||||
$sql = "SELECT p.id, p.nik, p.nama, p.jenis_kelamin, p.tanggal_lahir,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
p.status_keluarga, p.status_kawin, p.status_hidup,
|
||||
p.pekerjaan, p.penghasilan, p.status_ekonomi, p.kondisi_kesehatan,
|
||||
p.pendidikan_terakhir, p.status_pendidikan,
|
||||
p.lat, p.lng, p.keluarga_id,
|
||||
k.no_kk, k.alamat AS alamat_kk, k.kelurahan, k.kecamatan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id = p.id AND hb.status_penyaluran = 'disalurkan') AS total_bantuan_diterima,
|
||||
(SELECT MAX(hb2.tanggal_penyaluran) FROM histori_bantuan hb2 WHERE hb2.penduduk_id = p.id) AS terakhir_dibantu
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id = k.id
|
||||
WHERE $wStr
|
||||
ORDER BY FIELD(p.status_ekonomi,'miskin','rentan','mampu'), p.nama
|
||||
LIMIT 200";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
if (!$rows) jsonError('Query gagal: ' . $conn->error);
|
||||
jsonSuccess($rows->fetch_all(MYSQLI_ASSOC));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MAP — optimized for Leaflet markers
|
||||
// ============================================================
|
||||
if ($action === 'map') {
|
||||
$filter = sanitize($conn, $_GET['filter'] ?? 'all');
|
||||
$search = sanitize($conn, $_GET['search'] ?? '');
|
||||
|
||||
$where = ["p.status_hidup = 'hidup'", "p.lat IS NOT NULL", "p.lng IS NOT NULL"];
|
||||
|
||||
if ($filter === 'miskin') $where[] = "p.status_ekonomi = 'miskin'";
|
||||
elseif ($filter === 'rentan') $where[] = "p.status_ekonomi = 'rentan'";
|
||||
elseif ($filter === 'mampu') $where[] = "p.status_ekonomi = 'mampu'";
|
||||
elseif ($filter === 'prioritas') {
|
||||
$where[] = "(p.status_ekonomi = 'miskin' OR p.kondisi_kesehatan IN ('sakit_berat','disabilitas'))";
|
||||
}
|
||||
|
||||
if ($search) $where[] = "(p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')";
|
||||
$wStr = implode(' AND ', $where);
|
||||
|
||||
$sql = "SELECT p.id, p.nik, p.nama,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
p.status_ekonomi, p.kondisi_kesehatan, p.pekerjaan,
|
||||
p.lat, p.lng,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id = p.id AND hb.status_penyaluran = 'disalurkan') AS total_bantuan_diterima
|
||||
FROM penduduk p
|
||||
WHERE $wStr
|
||||
LIMIT 500";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
if (!$rows) jsonError('Query gagal: ' . $conn->error);
|
||||
jsonSuccess($rows->fetch_all(MYSQLI_ASSOC));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET — single penduduk dengan detail
|
||||
// ============================================================
|
||||
if ($action === 'get') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) jsonError('ID tidak valid');
|
||||
|
||||
$p = dbFirst($conn, "SELECT p.*,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
k.no_kk, k.alamat AS alamat_kk, k.rt, k.rw, k.kelurahan, k.kecamatan,
|
||||
ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id = p.id AND hb.status_penyaluran = 'disalurkan') AS total_bantuan_diterima,
|
||||
(SELECT MAX(hb2.tanggal_penyaluran) FROM histori_bantuan hb2 WHERE hb2.penduduk_id = p.id) AS terakhir_dibantu
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id = ri.id
|
||||
WHERE p.id = ?", [$id], 'i');
|
||||
|
||||
if (!$p) jsonError('Data tidak ditemukan', 404);
|
||||
|
||||
// Riwayat pendidikan
|
||||
$p['riwayat_pendidikan'] = dbQuery($conn, "SELECT * FROM riwayat_pendidikan WHERE penduduk_id=? ORDER BY tahun_masuk DESC", [$id], 'i');
|
||||
|
||||
// Histori bantuan
|
||||
$p['histori_bantuan'] = dbQuery($conn, "SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, u.nama AS nama_penyalur
|
||||
FROM histori_bantuan hb
|
||||
JOIN jenis_bantuan jb ON hb.jenis_bantuan_id = jb.id
|
||||
LEFT JOIN users u ON hb.disalurkan_oleh = u.id
|
||||
WHERE hb.penduduk_id = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC", [$id], 'i');
|
||||
|
||||
// Anggota keluarga lain
|
||||
if ($p['keluarga_id']) {
|
||||
$p['satu_keluarga'] = dbQuery($conn, "SELECT id, nik, nama, TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur, status_keluarga, pekerjaan, status_ekonomi FROM penduduk WHERE keluarga_id=? AND id!=? ORDER BY status_keluarga", [$p['keluarga_id'], $id], 'ii');
|
||||
}
|
||||
|
||||
jsonSuccess($p);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SAVE — INSERT / UPDATE
|
||||
// ============================================================
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin', 'pengurus');
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nik = sanitize($conn, $_POST['nik'] ?? '');
|
||||
$nama = sanitize($conn, $_POST['nama'] ?? '');
|
||||
$jk = sanitize($conn, $_POST['jenis_kelamin'] ?? 'L');
|
||||
$tgl_lahir = sanitize($conn, $_POST['tanggal_lahir'] ?? '');
|
||||
$tempat_lahir = sanitize($conn, $_POST['tempat_lahir'] ?? '');
|
||||
$status_kk = sanitize($conn, $_POST['status_keluarga'] ?? 'anak');
|
||||
$status_kawin = sanitize($conn, $_POST['status_kawin'] ?? 'belum_kawin');
|
||||
$status_hidup = sanitize($conn, $_POST['status_hidup'] ?? 'hidup');
|
||||
$tgl_meninggal = sanitize($conn, $_POST['tanggal_meninggal'] ?? '');
|
||||
$keluarga_id = (int)($_POST['keluarga_id'] ?? 0);
|
||||
$pendidikan = sanitize($conn, $_POST['pendidikan_terakhir'] ?? 'tidak_sekolah');
|
||||
$status_pddk = sanitize($conn, $_POST['status_pendidikan'] ?? 'tidak_sekolah');
|
||||
$pekerjaan = sanitize($conn, $_POST['pekerjaan'] ?? '');
|
||||
$penghasilan = (float)($_POST['penghasilan'] ?? 0);
|
||||
$status_ekonomi = sanitize($conn, $_POST['status_ekonomi'] ?? 'rentan');
|
||||
$kondisi_sehat = sanitize($conn, $_POST['kondisi_kesehatan'] ?? 'sehat');
|
||||
$catatan_sehat = sanitize($conn, $_POST['catatan_kesehatan'] ?? '');
|
||||
$lat = !empty($_POST['lat']) ? (float)$_POST['lat'] : null;
|
||||
$lng = !empty($_POST['lng']) ? (float)$_POST['lng'] : null;
|
||||
|
||||
// Validasi NIK
|
||||
if (strlen($nik) !== 16 || !ctype_digit($nik)) jsonError('NIK harus 16 digit angka');
|
||||
if (!$nama) jsonError('Nama tidak boleh kosong');
|
||||
if (!$tgl_lahir) jsonError('Tanggal lahir wajib diisi');
|
||||
|
||||
// Hitung umur untuk validasi pendidikan
|
||||
$umur = (int)date_diff(date_create($tgl_lahir), date_create())->y;
|
||||
$validasi = validasiPendidikan($umur, $status_pddk, $pendidikan);
|
||||
// Validasi non-blocking (hanya warning, tidak stop save)
|
||||
|
||||
// Upload foto
|
||||
$foto = '';
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$foto = uploadFile($_FILES['foto'], 'pddk', ['jpg','jpeg','png','webp']);
|
||||
}
|
||||
|
||||
$keluarga_val = $keluarga_id ? $keluarga_id : 'NULL';
|
||||
$tgl_meninggal_val = ($status_hidup === 'meninggal' && $tgl_meninggal) ? "'$tgl_meninggal'" : 'NULL';
|
||||
$lat_val = $lat !== null ? $lat : 'NULL';
|
||||
$lng_val = $lng !== null ? $lng : 'NULL';
|
||||
$foto_set = $foto ? "'$foto'" : ($id > 0 ? "(SELECT foto FROM penduduk WHERE id=$id)" : 'NULL');
|
||||
|
||||
if ($id > 0) {
|
||||
// UPDATE
|
||||
$sql = "UPDATE penduduk SET
|
||||
nik='$nik', nama='$nama', jenis_kelamin='$jk', tanggal_lahir='$tgl_lahir',
|
||||
tempat_lahir='$tempat_lahir', status_keluarga='$status_kk', status_kawin='$status_kawin',
|
||||
status_hidup='$status_hidup', tanggal_meninggal=$tgl_meninggal_val,
|
||||
keluarga_id=$keluarga_val, pendidikan_terakhir='$pendidikan', status_pendidikan='$status_pddk',
|
||||
pekerjaan='$pekerjaan', penghasilan=$penghasilan, status_ekonomi='$status_ekonomi',
|
||||
kondisi_kesehatan='$kondisi_sehat', catatan_kesehatan='$catatan_sehat',
|
||||
lat=$lat_val, lng=$lng_val,
|
||||
foto=$foto_set, updated_by=" . currentUserId() . "
|
||||
WHERE id=$id";
|
||||
|
||||
if (!$conn->query($sql)) jsonError('Gagal update: ' . $conn->error);
|
||||
logAktivitas($conn, 'UPDATE_PENDUDUK', 'penduduk', $id);
|
||||
jsonSuccess(['id' => $id, 'warnings' => $validasi], 'Data penduduk berhasil diperbarui');
|
||||
} else {
|
||||
// INSERT — cek NIK duplikat
|
||||
$existing = $conn->query("SELECT id FROM penduduk WHERE nik='$nik'")->fetch_assoc();
|
||||
if ($existing) jsonError('NIK sudah terdaftar di sistem');
|
||||
|
||||
$sql = "INSERT INTO penduduk (nik, nama, jenis_kelamin, tanggal_lahir, tempat_lahir,
|
||||
status_keluarga, status_kawin, status_hidup, tanggal_meninggal,
|
||||
keluarga_id, pendidikan_terakhir, status_pendidikan,
|
||||
pekerjaan, penghasilan, status_ekonomi, kondisi_kesehatan, catatan_kesehatan,
|
||||
lat, lng, foto, created_by)
|
||||
VALUES ('$nik','$nama','$jk','$tgl_lahir','$tempat_lahir',
|
||||
'$status_kk','$status_kawin','$status_hidup',$tgl_meninggal_val,
|
||||
$keluarga_val,'$pendidikan','$status_pddk',
|
||||
'$pekerjaan',$penghasilan,'$status_ekonomi','$kondisi_sehat','$catatan_sehat',
|
||||
$lat_val,$lng_val,$foto_set," . currentUserId() . ")";
|
||||
|
||||
if (!$conn->query($sql)) jsonError('Gagal simpan: ' . $conn->error);
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn, 'CREATE_PENDUDUK', 'penduduk', $newId);
|
||||
|
||||
// Update status ekonomi keluarga jika lebih parah
|
||||
if ($keluarga_id && $status_ekonomi === 'miskin') {
|
||||
$conn->query("UPDATE keluarga SET status_ekonomi='miskin' WHERE id=$keluarga_id AND status_ekonomi='rentan'");
|
||||
}
|
||||
|
||||
jsonSuccess(['id' => $newId, 'warnings' => $validasi], 'Data penduduk berhasil ditambahkan');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE
|
||||
// ============================================================
|
||||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if (!$id) jsonError('ID tidak valid');
|
||||
|
||||
if (!$conn->query("DELETE FROM penduduk WHERE id=$id")) {
|
||||
jsonError('Gagal hapus: ' . $conn->error);
|
||||
}
|
||||
logAktivitas($conn, 'DELETE_PENDUDUK', 'penduduk', $id);
|
||||
jsonSuccess(null, 'Data berhasil dihapus');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PRIORITAS — warga belum/sedikit dibantu, urut prioritas
|
||||
// ============================================================
|
||||
if ($action === 'prioritas') {
|
||||
$sql = "SELECT p.id, p.nik, p.nama, p.status_ekonomi, p.kondisi_kesehatan,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
k.kelurahan, k.kecamatan, k.lat, k.lng,
|
||||
ri.nama AS nama_ibadah,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id = p.id AND hb.status_penyaluran = 'disalurkan') AS total_bantuan,
|
||||
CASE
|
||||
WHEN p.status_ekonomi = 'miskin' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'DARURAT'
|
||||
WHEN p.status_ekonomi = 'miskin' THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi = 'rentan' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi = 'rentan' THEN 'SEDANG'
|
||||
ELSE 'RENDAH'
|
||||
END AS prioritas
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id = ri.id
|
||||
WHERE p.status_hidup = 'hidup'
|
||||
AND (p.status_ekonomi IN ('miskin','rentan') OR p.kondisi_kesehatan IN ('sakit_berat','disabilitas'))
|
||||
HAVING total_bantuan = 0
|
||||
ORDER BY FIELD(prioritas,'DARURAT','TINGGI','SEDANG','RENDAH'), p.status_ekonomi
|
||||
LIMIT 50";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal: ' . $action);
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
requireRole('admin','pengurus','pimpinan');
|
||||
$role = getRole();
|
||||
$uid = currentUserId();
|
||||
$riId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
$filterRumah = ($role==='pengurus' && $riId) ? "AND k.rumah_ibadah_id=$riId" : '';
|
||||
$search = sanitize($conn, $_GET['q'] ?? '');
|
||||
$jenis = sanitize($conn, $_GET['jenis'] ?? '');
|
||||
$wS = $search ? "AND (p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')" : '';
|
||||
$wJ = $jenis ? "AND pk.jenis='$jenis'" : '';
|
||||
$rows = $conn->query("
|
||||
SELECT pk.*, p.nama AS nama_penduduk, p.nik,
|
||||
TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
u.nama AS dicatat_nama, uv.nama AS diverifikasi_nama
|
||||
FROM peristiwa_kependudukan pk
|
||||
JOIN penduduk p ON pk.penduduk_id=p.id
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN users u ON pk.dicatat_oleh=u.id
|
||||
LEFT JOIN users uv ON pk.diverifikasi_oleh=uv.id
|
||||
WHERE 1=1 $filterRumah $wS $wJ
|
||||
ORDER BY pk.tanggal DESC, pk.created_at DESC LIMIT 200
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
}
|
||||
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$pid = (int)($_POST['penduduk_id'] ?? 0);
|
||||
$jenis = sanitize($conn, $_POST['jenis'] ?? '');
|
||||
$tgl = sanitize($conn, $_POST['tanggal'] ?? date('Y-m-d'));
|
||||
$ket = sanitize($conn, $_POST['keterangan'] ?? '');
|
||||
$asal = sanitize($conn, $_POST['alamat_asal'] ?? '');
|
||||
$tuju = sanitize($conn, $_POST['alamat_tujuan'] ?? '');
|
||||
$sebab = sanitize($conn, $_POST['sebab_meninggal'] ?? '');
|
||||
$oleh = currentUserId();
|
||||
if (!$pid || !$jenis) jsonError('Data tidak lengkap');
|
||||
$kidR = $conn->query("SELECT keluarga_id FROM penduduk WHERE id=$pid")->fetch_assoc();
|
||||
$kid = $kidR['keluarga_id'] ?? null;
|
||||
$dokumen = null;
|
||||
if (!empty($_FILES['dokumen']['name']))
|
||||
$dokumen = uploadFile($_FILES['dokumen'],'peristiwa',['jpg','jpeg','png','pdf']);
|
||||
$stmt = $conn->prepare("INSERT INTO peristiwa_kependudukan
|
||||
(penduduk_id,keluarga_id,jenis,tanggal,keterangan,alamat_asal,alamat_tujuan,sebab_meninggal,dokumen,dicatat_oleh)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->bind_param('iisssssssi',$pid,$kid,$jenis,$tgl,$ket,$asal,$tuju,$sebab,$dokumen,$oleh);
|
||||
if (!$stmt->execute()) jsonError($conn->error);
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn,'CREATE_PERISTIWA','peristiwa_kependudukan',$newId);
|
||||
// Sync ke penduduk
|
||||
if ($jenis==='meninggal')
|
||||
$conn->query("UPDATE penduduk SET status_hidup='meninggal',tanggal_meninggal='$tgl',updated_by=$oleh WHERE id=$pid");
|
||||
elseif ($jenis==='menikah')
|
||||
$conn->query("UPDATE penduduk SET status_kawin='kawin',updated_by=$oleh WHERE id=$pid");
|
||||
elseif ($jenis==='cerai')
|
||||
$conn->query("UPDATE penduduk SET status_kawin='cerai_hidup',updated_by=$oleh WHERE id=$pid");
|
||||
elseif ($jenis==='pindah_keluar')
|
||||
$conn->query("UPDATE penduduk SET keluarga_id=NULL,domisili_sama_kk=0,updated_by=$oleh WHERE id=$pid");
|
||||
jsonSuccess(['id'=>$newId],'Peristiwa berhasil dicatat');
|
||||
}
|
||||
|
||||
if ($action==='verifikasi' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$uid = currentUserId();
|
||||
$conn->query("UPDATE peristiwa_kependudukan SET diverifikasi=1,diverifikasi_oleh=$uid,diverifikasi_at=NOW() WHERE id=$id");
|
||||
logAktivitas($conn,'VERIFIKASI_PERISTIWA','peristiwa_kependudukan',$id);
|
||||
jsonSuccess(null,'Berhasil diverifikasi');
|
||||
}
|
||||
|
||||
if ($action==='delete' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$conn->query("DELETE FROM peristiwa_kependudukan WHERE id=$id");
|
||||
logAktivitas($conn,'DELETE_PERISTIWA','peristiwa_kependudukan',$id);
|
||||
jsonSuccess(null,'Berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
// api/pesan.php — Chat antar pengurus rumah ibadah
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
$ibadah_id = (int)($_GET['rumah_ibadah_id'] ?? 0);
|
||||
$limit = (int)($_GET['limit'] ?? 50);
|
||||
$where = $ibadah_id ? "AND m.rumah_ibadah_id=$ibadah_id" : "AND m.penerima_id IS NULL";
|
||||
|
||||
$sql = "SELECT m.*, u.nama AS nama_pengirim, ri.nama AS nama_ibadah
|
||||
FROM pesan m
|
||||
JOIN users u ON m.pengirim_id = u.id
|
||||
LEFT JOIN rumah_ibadah ri ON m.rumah_ibadah_id = ri.id
|
||||
WHERE 1=1 $where
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $limit";
|
||||
|
||||
$rows = $conn->query($sql);
|
||||
$data = $rows ? array_reverse($rows->fetch_all(MYSQLI_ASSOC)) : [];
|
||||
jsonSuccess($data);
|
||||
}
|
||||
|
||||
if ($action === 'send' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin', 'pengurus', 'pimpinan');
|
||||
$isi = sanitize($conn, $_POST['isi'] ?? '');
|
||||
$penerima = (int)($_POST['penerima_id'] ?? 0) ?: 'NULL';
|
||||
$ibadah_id = (int)($_POST['rumah_ibadah_id'] ?? 0) ?: 'NULL';
|
||||
$uid = currentUserId();
|
||||
|
||||
if (!$isi) jsonError('Pesan tidak boleh kosong');
|
||||
|
||||
// Auto set ibadah dari user jika belum diset
|
||||
if ($ibadah_id === 'NULL') {
|
||||
$u = dbFirst($conn, "SELECT rumah_ibadah_id FROM users WHERE id=?", [$uid], 'i');
|
||||
if ($u && $u['rumah_ibadah_id']) $ibadah_id = (int)$u['rumah_ibadah_id'];
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO pesan (pengirim_id, penerima_id, rumah_ibadah_id, isi) VALUES ($uid, $penerima, $ibadah_id, '$isi')";
|
||||
if (!$conn->query($sql)) jsonError('Gagal kirim: ' . $conn->error);
|
||||
|
||||
jsonSuccess(['id' => $conn->insert_id], 'Pesan terkirim');
|
||||
}
|
||||
|
||||
if ($action === 'unread') {
|
||||
$uid = currentUserId();
|
||||
$count = $conn->query("SELECT COUNT(*) c FROM pesan WHERE penerima_id=$uid AND dibaca=0")->fetch_assoc()['c'];
|
||||
jsonSuccess(['count' => (int)$count]);
|
||||
}
|
||||
|
||||
if ($action === 'baca' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$uid = currentUserId();
|
||||
$conn->query("UPDATE pesan SET dibaca=1 WHERE penerima_id=$uid AND dibaca=0");
|
||||
jsonSuccess(null, 'Ditandai sudah dibaca');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action==='list') {
|
||||
$rows = $conn->query("SELECT * FROM rekomendasi_pelatihan ORDER BY kategori,nama")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
}
|
||||
|
||||
if ($action==='list_penduduk') {
|
||||
requireRole('admin','pengurus');
|
||||
$role = getRole(); $riId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
$filterRumah = ($role==='pengurus' && $riId) ? "AND k.rumah_ibadah_id=$riId" : '';
|
||||
$search = sanitize($conn, $_GET['q'] ?? '');
|
||||
$wS = $search ? "AND (p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')" : '';
|
||||
$rows = $conn->query("
|
||||
SELECT p.id,p.nama,p.nik,p.jenis_kelamin,p.status_ekonomi,p.kondisi_kesehatan,
|
||||
p.kebutuhan_pelatihan, TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
ri.nama AS ibadah
|
||||
FROM penduduk p LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id
|
||||
WHERE p.status_hidup='hidup' $filterRumah $wS ORDER BY p.nama LIMIT 200
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
}
|
||||
|
||||
if ($action==='list_riwayat') {
|
||||
requireRole('admin','pengurus','pimpinan');
|
||||
$pid = (int)($_GET['penduduk_id'] ?? 0);
|
||||
$wP = $pid ? "AND pp.penduduk_id=$pid" : '';
|
||||
$rows = $conn->query("
|
||||
SELECT pp.*,p.nama AS nama_penduduk,rp.nama AS nama_pelatihan,
|
||||
rp.kategori,rp.penyelenggara,rp.durasi,
|
||||
u.nama AS direkomendasikan_oleh
|
||||
FROM penduduk_pelatihan pp
|
||||
JOIN penduduk p ON pp.penduduk_id=p.id
|
||||
JOIN rekomendasi_pelatihan rp ON pp.rekomendasi_pelatihan_id=rp.id
|
||||
LEFT JOIN users u ON pp.direkomendasikan_oleh=u.id
|
||||
WHERE 1=1 $wP ORDER BY pp.tanggal_rekomendasi DESC LIMIT 200
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
}
|
||||
|
||||
if ($action==='save_master' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$nama = sanitize($conn, trim($_POST['nama'] ?? ''));
|
||||
$kat = sanitize($conn, $_POST['kategori'] ?? 'vokasi');
|
||||
$desk = sanitize($conn, $_POST['deskripsi'] ?? '');
|
||||
$penye = sanitize($conn, $_POST['penyelenggara'] ?? '');
|
||||
$kontak = sanitize($conn, $_POST['kontak_penyelenggara'] ?? '');
|
||||
$umin = (int)($_POST['usia_min'] ?? 15);
|
||||
$umaks = (int)($_POST['usia_maks'] ?? 60);
|
||||
$gender = sanitize($conn, $_POST['gender'] ?? 'semua');
|
||||
$sasaran = sanitize($conn, $_POST['status_ekonomi_sasaran'] ?? 'semua');
|
||||
$prsyrt = sanitize($conn, $_POST['prasyarat'] ?? '');
|
||||
$durasi = sanitize($conn, $_POST['durasi'] ?? '');
|
||||
$biaya = floatval($_POST['biaya'] ?? 0);
|
||||
if (!$nama) jsonError('Nama pelatihan wajib diisi');
|
||||
$conn->query("INSERT INTO rekomendasi_pelatihan (nama,kategori,deskripsi,penyelenggara,kontak_penyelenggara,usia_min,usia_maks,gender,status_ekonomi_sasaran,prasyarat,durasi,biaya) VALUES ('$nama','$kat','$desk','$penye','$kontak',$umin,$umaks,'$gender','$sasaran','$prsyrt','$durasi',$biaya)");
|
||||
logAktivitas($conn,'CREATE_REKOMENDASI','rekomendasi_pelatihan',$conn->insert_id);
|
||||
jsonSuccess(['id'=>$conn->insert_id],"Pelatihan '$nama' berhasil ditambahkan");
|
||||
}
|
||||
|
||||
if ($action==='rekomendasikan' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$pid = (int)($_POST['penduduk_id'] ?? 0);
|
||||
$rekId = (int)($_POST['rekomendasi_pelatihan_id'] ?? 0);
|
||||
$cat = sanitize($conn, $_POST['catatan'] ?? '');
|
||||
$oleh = currentUserId();
|
||||
if (!$pid || !$rekId) jsonError('Data tidak lengkap');
|
||||
$cek = $conn->query("SELECT id FROM penduduk_pelatihan WHERE penduduk_id=$pid AND rekomendasi_pelatihan_id=$rekId AND status NOT IN ('selesai','batal')")->fetch_assoc();
|
||||
if ($cek) jsonError('Pelatihan ini sudah direkomendasikan dan masih aktif');
|
||||
$tgl = date('Y-m-d');
|
||||
$conn->query("INSERT INTO penduduk_pelatihan (penduduk_id,rekomendasi_pelatihan_id,status,tanggal_rekomendasi,catatan,direkomendasikan_oleh) VALUES ($pid,$rekId,'direkomendasikan','$tgl','$cat',$oleh)");
|
||||
logAktivitas($conn,'REKOMENDASIKAN_PELATIHAN','penduduk_pelatihan',$conn->insert_id);
|
||||
jsonSuccess(['id'=>$conn->insert_id],'Rekomendasi berhasil dikirim');
|
||||
}
|
||||
|
||||
if ($action==='update_status' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status = sanitize($conn, $_POST['status'] ?? '');
|
||||
$tMul = sanitize($conn, $_POST['tanggal_mulai'] ?? '');
|
||||
$tSls = sanitize($conn, $_POST['tanggal_selesai'] ?? '');
|
||||
$sertif = null;
|
||||
if (!empty($_FILES['sertifikat']['name']))
|
||||
$sertif = uploadFile($_FILES['sertifikat'],'sertif',['jpg','jpeg','png','pdf']);
|
||||
$setMul = $tMul ? ",tanggal_mulai='$tMul'" : '';
|
||||
$setSls = $tSls ? ",tanggal_selesai='$tSls'" : '';
|
||||
$setSrt = $sertif ? ",sertifikat='$sertif'" : '';
|
||||
$conn->query("UPDATE penduduk_pelatihan SET status='$status' $setMul $setSls $setSrt WHERE id=$id");
|
||||
logAktivitas($conn,'UPDATE_STATUS_PELATIHAN','penduduk_pelatihan',$id);
|
||||
jsonSuccess(null,'Status diperbarui');
|
||||
}
|
||||
|
||||
if ($action==='toggle_aktif' && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
requireRole('admin','pengurus');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$val = (int)($_POST['aktif'] ?? 0);
|
||||
$conn->query("UPDATE rekomendasi_pelatihan SET aktif=$val WHERE id=$id");
|
||||
jsonSuccess(null,$val?'Diaktifkan':'Dinonaktifkan');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// api/riwayat_pendidikan.php — CRUD Riwayat Pendidikan & Pelatihan
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
requireRole('admin','pengurus');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
$search = sanitize($conn, $_GET['search'] ?? '');
|
||||
$where = $search ? "AND (p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')" : '';
|
||||
$sql = "SELECT rp.*, p.nama AS nama_penduduk, p.nik,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur_penduduk
|
||||
FROM riwayat_pendidikan rp
|
||||
JOIN penduduk p ON rp.penduduk_id = p.id
|
||||
WHERE 1=1 $where
|
||||
ORDER BY rp.tahun_masuk DESC, p.nama
|
||||
LIMIT 200";
|
||||
$rows = $conn->query($sql);
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'get_by_penduduk') {
|
||||
$pid = (int)($_GET['penduduk_id'] ?? 0);
|
||||
if (!$pid) jsonError('penduduk_id wajib');
|
||||
$rows = $conn->query("SELECT * FROM riwayat_pendidikan WHERE penduduk_id=$pid ORDER BY tahun_masuk DESC");
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$penduduk_id = (int)($_POST['penduduk_id'] ?? 0);
|
||||
$jenis = sanitize($conn, $_POST['jenis'] ?? 'formal');
|
||||
$institusi = sanitize($conn, $_POST['nama_institusi'] ?? '');
|
||||
$jenjang = sanitize($conn, $_POST['jenjang'] ?? '');
|
||||
$jurusan = sanitize($conn, $_POST['jurusan'] ?? '');
|
||||
$thn_masuk = (int)($_POST['tahun_masuk'] ?? 0);
|
||||
$thn_lulus = !empty($_POST['tahun_lulus']) ? (int)$_POST['tahun_lulus'] : null;
|
||||
$status = sanitize($conn, $_POST['status'] ?? 'lulus');
|
||||
|
||||
if (!$penduduk_id) jsonError('penduduk_id wajib');
|
||||
|
||||
// Validasi logika
|
||||
$p = dbFirst($conn, "SELECT TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) AS umur FROM penduduk WHERE id=?", [$penduduk_id], 'i');
|
||||
$umur = $p ? (int)$p['umur'] : 0;
|
||||
$warns = validasiPendidikan($umur, $status === 'sedang_berjalan' ? 'masih_sekolah' : 'lulus', strtolower($jenjang));
|
||||
|
||||
$thn_lulus_val = $thn_lulus ? $thn_lulus : 'NULL';
|
||||
$thn_masuk_val = $thn_masuk ?: 'NULL';
|
||||
|
||||
if ($id > 0) {
|
||||
$conn->query("UPDATE riwayat_pendidikan SET jenis='$jenis',nama_institusi='$institusi',jenjang='$jenjang',jurusan='$jurusan',tahun_masuk=$thn_masuk_val,tahun_lulus=$thn_lulus_val,status='$status' WHERE id=$id AND penduduk_id=$penduduk_id");
|
||||
logAktivitas($conn, 'UPDATE_RIWAYAT_PDDK', 'riwayat_pendidikan', $id);
|
||||
jsonSuccess(['id' => $id, 'warnings' => $warns], 'Riwayat pendidikan diperbarui');
|
||||
} else {
|
||||
$conn->query("INSERT INTO riwayat_pendidikan (penduduk_id,jenis,nama_institusi,jenjang,jurusan,tahun_masuk,tahun_lulus,status) VALUES ($penduduk_id,'$jenis','$institusi','$jenjang','$jurusan',$thn_masuk_val,$thn_lulus_val,'$status')");
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn, 'CREATE_RIWAYAT_PDDK', 'riwayat_pendidikan', $newId);
|
||||
jsonSuccess(['id' => $newId, 'warnings' => $warns], 'Riwayat pendidikan ditambahkan');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$conn->query("DELETE FROM riwayat_pendidikan WHERE id=$id");
|
||||
logAktivitas($conn, 'DELETE_RIWAYAT_PDDK', 'riwayat_pendidikan', $id);
|
||||
jsonSuccess(null, 'Dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
// api/rumah_ibadah.php — CRUD Rumah Ibadah
|
||||
require_once '../koneksi.php';
|
||||
requireLogin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
if ($action === 'list') {
|
||||
$rows = $conn->query("SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id = ri.id) AS jumlah_kk,
|
||||
(SELECT COUNT(*) FROM keluarga k2 JOIN penduduk p ON p.keluarga_id = k2.id WHERE k2.rumah_ibadah_id = ri.id AND p.status_ekonomi = 'miskin') AS jumlah_miskin
|
||||
FROM rumah_ibadah ri WHERE ri.aktif=1 ORDER BY ri.nama");
|
||||
jsonSuccess($rows ? $rows->fetch_all(MYSQLI_ASSOC) : []);
|
||||
}
|
||||
|
||||
if ($action === 'get') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$ri = dbFirst($conn, "SELECT * FROM rumah_ibadah WHERE id=?", [$id], 'i');
|
||||
if (!$ri) jsonError('Tidak ditemukan', 404);
|
||||
$ri['keluarga'] = dbQuery($conn, "SELECT k.*, (SELECT COUNT(*) FROM penduduk p WHERE p.keluarga_id=k.id) AS jumlah_anggota FROM keluarga k WHERE k.rumah_ibadah_id=?", [$id], 'i');
|
||||
jsonSuccess($ri);
|
||||
}
|
||||
|
||||
if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = sanitize($conn, $_POST['nama'] ?? '');
|
||||
$jenis = sanitize($conn, $_POST['jenis'] ?? 'Masjid');
|
||||
$kontak = sanitize($conn, $_POST['kontak'] ?? '');
|
||||
$alamat = sanitize($conn, $_POST['alamat'] ?? '');
|
||||
$kel = sanitize($conn, $_POST['kelurahan'] ?? '');
|
||||
$kec = sanitize($conn, $_POST['kecamatan'] ?? '');
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
$radius = (int)($_POST['radius'] ?? 500);
|
||||
if (!$nama) jsonError('Nama wajib diisi');
|
||||
if (!$lat || !$lng) jsonError('Koordinat wajib diisi');
|
||||
if ($id > 0) {
|
||||
$conn->query("UPDATE rumah_ibadah SET nama='$nama',jenis='$jenis',kontak='$kontak',alamat='$alamat',kelurahan='$kel',kecamatan='$kec',lat=$lat,lng=$lng,radius=$radius WHERE id=$id");
|
||||
logAktivitas($conn, 'UPDATE_IBADAH', 'rumah_ibadah', $id);
|
||||
jsonSuccess(['id'=>$id], 'Berhasil diperbarui');
|
||||
} else {
|
||||
$conn->query("INSERT INTO rumah_ibadah (nama,jenis,kontak,alamat,kelurahan,kecamatan,lat,lng,radius) VALUES ('$nama','$jenis','$kontak','$alamat','$kel','$kec',$lat,$lng,$radius)");
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn, 'CREATE_IBADAH', 'rumah_ibadah', $newId);
|
||||
jsonSuccess(['id'=>$newId], 'Rumah ibadah berhasil ditambahkan');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
requireRole('admin');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$conn->query("UPDATE rumah_ibadah SET aktif=0 WHERE id=$id");
|
||||
logAktivitas($conn, 'DELETE_IBADAH', 'rumah_ibadah', $id);
|
||||
jsonSuccess(null, 'Berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Action tidak dikenal');
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
require_once 'koneksi.php';
|
||||
requireLogin();
|
||||
requireRole('admin','pengurus','pimpinan');
|
||||
|
||||
// Fetch all stats server-side
|
||||
$totalPenduduk = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE status_hidup='hidup'")->fetch_assoc()['c'];
|
||||
$totalMiskin = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup'")->fetch_assoc()['c'];
|
||||
$totalRentan = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup'")->fetch_assoc()['c'];
|
||||
$totalMampu = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE status_ekonomi='mampu' AND status_hidup='hidup'")->fetch_assoc()['c'];
|
||||
$totalKeluarga = $conn->query("SELECT COUNT(*) c FROM keluarga")->fetch_assoc()['c'];
|
||||
$totalBantuan = $conn->query("SELECT COUNT(*) c FROM histori_bantuan WHERE status_penyaluran='disalurkan'")->fetch_assoc()['c'];
|
||||
$nilaiSalur = $conn->query("SELECT COALESCE(SUM(nilai_rupiah),0) c FROM histori_bantuan WHERE status_penyaluran='disalurkan'")->fetch_assoc()['c'];
|
||||
$totalLaporan = $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='pending'")->fetch_assoc()['c'];
|
||||
$belumDibantu = $conn->query("SELECT COUNT(DISTINCT p.id) c FROM penduduk p WHERE p.status_ekonomi='miskin' AND p.status_hidup='hidup' AND p.id NOT IN (SELECT DISTINCT penduduk_id FROM histori_bantuan WHERE status_penyaluran='disalurkan')")->fetch_assoc()['c'];
|
||||
|
||||
// Charts data
|
||||
$perKecamatan = $conn->query("SELECT k.kecamatan, COUNT(p.id) total, SUM(p.status_ekonomi='miskin') miskin, SUM(p.status_ekonomi='rentan') rentan, SUM(p.status_ekonomi='mampu') mampu FROM penduduk p JOIN keluarga k ON p.keluarga_id=k.id WHERE p.status_hidup='hidup' AND k.kecamatan!='' GROUP BY k.kecamatan ORDER BY total DESC LIMIT 10")->fetch_all(MYSQLI_ASSOC);
|
||||
$perBulan = $conn->query("SELECT DATE_FORMAT(tanggal_penyaluran,'%Y-%m') bln, COUNT(*) total, SUM(nilai_rupiah) nilai FROM histori_bantuan WHERE status_penyaluran='disalurkan' AND tanggal_penyaluran >= DATE_SUB(NOW(), INTERVAL 12 MONTH) GROUP BY bln ORDER BY bln")->fetch_all(MYSQLI_ASSOC);
|
||||
$perIbadah = $conn->query("SELECT ri.nama, ri.jenis, COUNT(hb.id) total FROM histori_bantuan hb JOIN rumah_ibadah ri ON hb.rumah_ibadah_id=ri.id GROUP BY ri.id ORDER BY total DESC")->fetch_all(MYSQLI_ASSOC);
|
||||
$perPendidikan = $conn->query("SELECT pendidikan_terakhir, COUNT(*) total FROM penduduk WHERE status_hidup='hidup' GROUP BY pendidikan_terakhir ORDER BY FIELD(pendidikan_terakhir,'tidak_sekolah','sd','smp','sma','smk','d1','d2','d3','s1','s2','s3')")->fetch_all(MYSQLI_ASSOC);
|
||||
$anakTidakSek = $conn->query("SELECT COUNT(*) c FROM penduduk WHERE status_hidup='hidup' AND TIMESTAMPDIFF(YEAR,tanggal_lahir,CURDATE()) BETWEEN 7 AND 15 AND status_pendidikan NOT IN ('masih_sekolah')")->fetch_assoc()['c'];
|
||||
$laporanStatus = $conn->query("SELECT status, COUNT(*) total FROM laporan GROUP BY status")->fetch_all(MYSQLI_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Dashboard Analitik — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;
|
||||
--border:#243044;--border2:#2f3f57;
|
||||
--text:#e8edf3;--muted:#6b7e96;
|
||||
--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--purple:#9b59b6;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;
|
||||
}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s,color .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(232,80,80,.12);color:var(--red);}
|
||||
.nav-user{font-size:12px;color:var(--muted);}
|
||||
.container{max-width:1400px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:22px;}
|
||||
h3{font-family:var(--font-head);font-size:14px;font-weight:700;margin-bottom:14px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;}
|
||||
|
||||
/* KPI GRID */
|
||||
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:28px;}
|
||||
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:18px 20px;position:relative;overflow:hidden;}
|
||||
.kpi::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:currentColor;opacity:.6;}
|
||||
.kpi-value{font-family:var(--font-head);font-size:32px;font-weight:800;line-height:1;margin-bottom:6px;}
|
||||
.kpi-label{font-size:12px;color:var(--muted);font-weight:500;}
|
||||
.kpi-sub{font-size:11px;color:var(--muted);margin-top:6px;opacity:.7;}
|
||||
.c-red{color:var(--red);}
|
||||
.c-yellow{color:var(--yellow);}
|
||||
.c-green{color:var(--green);}
|
||||
.c-blue{color:var(--blue);}
|
||||
.c-purple{color:var(--purple);}
|
||||
.c-orange{color:#e67e22;}
|
||||
|
||||
/* ALERT */
|
||||
.alert{border-radius:12px;padding:14px 18px;font-size:13px;margin-bottom:22px;display:flex;align-items:center;gap:10px;}
|
||||
.alert-danger{background:rgba(232,80,80,.1);border:1px solid rgba(232,80,80,.3);color:#ff8080;}
|
||||
.alert-warning{background:rgba(240,180,41,.08);border:1px solid rgba(240,180,41,.25);color:var(--yellow);}
|
||||
|
||||
/* CHART GRID */
|
||||
.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:28px;}
|
||||
.chart-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:22px;}
|
||||
.chart-wrap{position:relative;height:260px;}
|
||||
.chart-full{grid-column:1/-1;}
|
||||
.chart-wrap canvas{max-height:260px;}
|
||||
|
||||
/* TABLE */
|
||||
.tbl-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:22px;margin-bottom:28px;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
th{text-align:left;padding:8px 12px;background:var(--surface2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border);}
|
||||
td{padding:9px 12px;border-bottom:1px solid var(--border2);}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;text-transform:uppercase;}
|
||||
.b-miskin{background:rgba(232,80,80,.2);color:var(--red);}
|
||||
.b-rentan{background:rgba(240,180,41,.2);color:var(--yellow);}
|
||||
.b-mampu{background:rgba(39,174,114,.2);color:var(--green);}
|
||||
.bar-container{background:var(--surface2);border-radius:4px;height:8px;overflow:hidden;}
|
||||
.bar-fill{height:100%;border-radius:4px;transition:width .5s ease;}
|
||||
|
||||
@media(max-width:900px){.chart-grid{grid-template-columns:1fr;}.kpi-grid{grid-template-columns:repeat(2,1fr);}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link active">Dashboard</a>
|
||||
<a href="keluarga.php" class="nav-link">Keluarga</a>
|
||||
<?php if(hasRole('admin','pengurus')): ?>
|
||||
<a href="riwayat_pendidikan.php" class="nav-link">Pendidikan</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="nav-user">👤 <?= htmlspecialchars($_SESSION['nama']) ?> (<?= $_SESSION['role_nama'] ?>)</div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none;margin-left:10px">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>📊 Dashboard Analitik Pengentasan Kemiskinan</h2>
|
||||
|
||||
<?php if($belumDibantu > 0): ?>
|
||||
<div class="alert alert-danger">
|
||||
🚨 <strong><?= $belumDibantu ?> warga miskin</strong> belum pernah menerima bantuan apapun. Segera tindak lanjuti!
|
||||
<a href="index.php" style="color:var(--red);margin-left:auto;font-weight:600;font-size:12px">→ Lihat di Peta</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($anakTidakSek > 0): ?>
|
||||
<div class="alert alert-warning">
|
||||
⚠️ <strong><?= $anakTidakSek ?> anak usia 7–15 tahun</strong> terdeteksi tidak/belum sekolah. Butuh verifikasi dan intervensi.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($totalLaporan > 0): ?>
|
||||
<div class="alert alert-warning">
|
||||
📢 <strong><?= $totalLaporan ?> laporan masyarakat</strong> masih berstatus <em>pending</em>. Harap segera ditindaklanjuti.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi c-blue">
|
||||
<div class="kpi-value"><?= number_format($totalPenduduk) ?></div>
|
||||
<div class="kpi-label">Total Penduduk</div>
|
||||
<div class="kpi-sub"><?= number_format($totalKeluarga) ?> kartu keluarga</div>
|
||||
</div>
|
||||
<div class="kpi c-red">
|
||||
<div class="kpi-value"><?= number_format($totalMiskin) ?></div>
|
||||
<div class="kpi-label">Warga Miskin</div>
|
||||
<div class="kpi-sub"><?= $totalPenduduk ? round($totalMiskin/$totalPenduduk*100,1) : 0 ?>% dari total</div>
|
||||
</div>
|
||||
<div class="kpi c-yellow">
|
||||
<div class="kpi-value"><?= number_format($totalRentan) ?></div>
|
||||
<div class="kpi-label">Warga Rentan</div>
|
||||
<div class="kpi-sub"><?= $totalPenduduk ? round($totalRentan/$totalPenduduk*100,1) : 0 ?>% dari total</div>
|
||||
</div>
|
||||
<div class="kpi c-green">
|
||||
<div class="kpi-value"><?= number_format($totalBantuan) ?></div>
|
||||
<div class="kpi-label">Bantuan Tersalurkan</div>
|
||||
<div class="kpi-sub">Rp <?= number_format($nilaiSalur,0,',','.') ?></div>
|
||||
</div>
|
||||
<div class="kpi c-orange">
|
||||
<div class="kpi-value"><?= number_format($belumDibantu) ?></div>
|
||||
<div class="kpi-label">Miskin Belum Dibantu</div>
|
||||
<div class="kpi-sub"><?= $totalMiskin ? round($belumDibantu/$totalMiskin*100,1) : 0 ?>% dari miskin</div>
|
||||
</div>
|
||||
<div class="kpi c-purple">
|
||||
<div class="kpi-value"><?= number_format($totalLaporan) ?></div>
|
||||
<div class="kpi-label">Laporan Pending</div>
|
||||
<div class="kpi-sub">Perlu ditindaklanjuti</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CHARTS ROW 1 -->
|
||||
<div class="chart-grid">
|
||||
<div class="chart-card">
|
||||
<h3>Distribusi Status Ekonomi</h3>
|
||||
<div class="chart-wrap"><canvas id="chartEkonomi"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Tingkat Pendidikan Penduduk</h3>
|
||||
<div class="chart-wrap"><canvas id="chartPendidikan"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card chart-full">
|
||||
<h3>Trend Penyaluran Bantuan (12 Bulan Terakhir)</h3>
|
||||
<div class="chart-wrap"><canvas id="chartBulan"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Status Laporan Masyarakat</h3>
|
||||
<div class="chart-wrap"><canvas id="chartLaporan"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Penyaluran per Rumah Ibadah</h3>
|
||||
<div class="chart-wrap"><canvas id="chartIbadah"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABLE SEBARAN PER KECAMATAN -->
|
||||
<div class="tbl-card">
|
||||
<h3>Sebaran Warga per Kecamatan</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Kecamatan</th><th>Total</th><th>Miskin</th><th>Rentan</th><th>Mampu</th><th>% Miskin</th><th>Proporsi</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($perKecamatan as $row):
|
||||
$pct = $row['total'] > 0 ? round($row['miskin']/$row['total']*100,1) : 0;
|
||||
$barW = $row['total'] > 0 ? round($row['miskin']/$row['total']*100) : 0;
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($row['kecamatan']) ?></strong></td>
|
||||
<td><?= number_format($row['total']) ?></td>
|
||||
<td><span class="badge b-miskin"><?= $row['miskin'] ?></span></td>
|
||||
<td><span class="badge b-rentan"><?= $row['rentan'] ?></span></td>
|
||||
<td><span class="badge b-mampu"><?= $row['mampu'] ?></span></td>
|
||||
<td style="color:<?= $pct>30?'var(--red)':($pct>15?'var(--yellow)':'var(--green)') ?>;font-weight:600"><?= $pct ?>%</td>
|
||||
<td style="width:140px">
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" style="width:<?= $barW ?>%;background:var(--red)"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
Chart.defaults.color = '#6b7e96';
|
||||
Chart.defaults.borderColor = '#243044';
|
||||
Chart.defaults.font.family = 'Inter';
|
||||
|
||||
const palette = {
|
||||
red: '#e85050', yellow: '#f0b429', green: '#27ae72',
|
||||
blue: '#4a9eff', purple: '#9b59b6', orange: '#e67e22'
|
||||
};
|
||||
|
||||
// Ekonomi donut
|
||||
new Chart(document.getElementById('chartEkonomi'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Miskin', 'Rentan', 'Mampu'],
|
||||
datasets: [{
|
||||
data: [<?= $totalMiskin ?>, <?= $totalRentan ?>, <?= $totalMampu ?>],
|
||||
backgroundColor: [palette.red, palette.yellow, palette.green],
|
||||
borderWidth: 0, hoverOffset: 8
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { position: 'bottom' } },
|
||||
cutout: '65%',
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Pendidikan bar
|
||||
const pddk = <?= json_encode(array_column($perPendidikan,'pendidikan_terakhir')) ?>;
|
||||
const pddkVal = <?= json_encode(array_column($perPendidikan,'total')) ?>;
|
||||
const pddkLabel = pddk.map(p => p.replace('_',' ').toUpperCase());
|
||||
new Chart(document.getElementById('chartPendidikan'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: pddkLabel,
|
||||
datasets: [{
|
||||
label: 'Jumlah',
|
||||
data: pddkVal,
|
||||
backgroundColor: pddkLabel.map((_, i) => `hsl(${200+i*18},70%,55%)`),
|
||||
borderRadius: 6, borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { x: { grid: { display: false } }, y: { grid: { color: '#243044' } } },
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Trend bantuan per bulan
|
||||
const bulanLabels = <?= json_encode(array_column($perBulan,'bln')) ?>;
|
||||
const bulanTotal = <?= json_encode(array_map('intval', array_column($perBulan,'total'))) ?>;
|
||||
const bulanNilai = <?= json_encode(array_map('floatval', array_column($perBulan,'nilai'))) ?>;
|
||||
new Chart(document.getElementById('chartBulan'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: bulanLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Jumlah Bantuan',
|
||||
data: bulanTotal,
|
||||
borderColor: palette.blue,
|
||||
backgroundColor: 'rgba(74,158,255,0.08)',
|
||||
fill: true, tension: 0.4, pointRadius: 5, yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Nilai (Rp)',
|
||||
data: bulanNilai,
|
||||
borderColor: palette.green,
|
||||
backgroundColor: 'rgba(39,174,114,0.06)',
|
||||
fill: true, tension: 0.4, pointRadius: 5, yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { position: 'top' } },
|
||||
scales: {
|
||||
x: { grid: { color: '#243044' } },
|
||||
y: { position: 'left', grid: { color: '#243044' }, title: { display: true, text: 'Jumlah', color: '#6b7e96' } },
|
||||
y1: { position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: 'Nilai (Rp)', color: '#6b7e96' } }
|
||||
},
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Status laporan
|
||||
const lapStatus = <?= json_encode($laporanStatus) ?>;
|
||||
const lapColors = { pending: palette.purple, diverifikasi: palette.blue, diproses: palette.yellow, selesai: palette.green, ditolak: palette.red };
|
||||
new Chart(document.getElementById('chartLaporan'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: lapStatus.map(l => l.status),
|
||||
datasets: [{
|
||||
data: lapStatus.map(l => l.total),
|
||||
backgroundColor: lapStatus.map(l => lapColors[l.status] || palette.blue),
|
||||
borderWidth: 0, hoverOffset: 6
|
||||
}]
|
||||
},
|
||||
options: { plugins: { legend: { position: 'bottom' } }, maintainAspectRatio: false }
|
||||
});
|
||||
|
||||
// Penyaluran per rumah ibadah
|
||||
const ibadahData = <?= json_encode($perIbadah) ?>;
|
||||
new Chart(document.getElementById('chartIbadah'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ibadahData.map(r => r.nama.length > 20 ? r.nama.substring(0,20)+'…' : r.nama),
|
||||
datasets: [{
|
||||
label: 'Bantuan Tersalurkan',
|
||||
data: ibadahData.map(r => r.total),
|
||||
backgroundColor: [palette.blue, palette.green, palette.purple, palette.orange, palette.yellow, palette.red],
|
||||
borderRadius: 6, borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { x: { grid: { color: '#243044' } }, y: { grid: { display: false } } },
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
-- ============================================================
|
||||
-- WebGIS UAS — INSTALL / REINSTALL DATABASE
|
||||
-- Jalankan file ini untuk setup database dari awal
|
||||
-- Data yang sudah ada akan DIMIGRASI (tidak hilang)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgisuas CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgisuas;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================================
|
||||
-- LANGKAH 1: DROP tabel lama yang mungkin corrupt
|
||||
-- ============================================================
|
||||
DROP TABLE IF EXISTS histori_bantuan;
|
||||
DROP TABLE IF EXISTS jadwal_piket;
|
||||
DROP TABLE IF EXISTS jenis_bantuan;
|
||||
DROP TABLE IF EXISTS keluarga;
|
||||
DROP TABLE IF EXISTS laporan;
|
||||
DROP TABLE IF EXISTS log_aktivitas;
|
||||
DROP TABLE IF EXISTS notifikasi;
|
||||
DROP TABLE IF EXISTS penduduk;
|
||||
DROP TABLE IF EXISTS penduduk_pelatihan;
|
||||
DROP TABLE IF EXISTS peristiwa_kependudukan;
|
||||
DROP TABLE IF EXISTS pesan;
|
||||
DROP TABLE IF EXISTS program_bantuan;
|
||||
DROP TABLE IF EXISTS rekomendasi_pelatihan;
|
||||
DROP TABLE IF EXISTS riwayat_pendidikan;
|
||||
DROP TABLE IF EXISTS roles;
|
||||
DROP TABLE IF EXISTS rumah_ibadah;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP VIEW IF EXISTS v_laporan_aktif;
|
||||
DROP VIEW IF EXISTS v_penduduk;
|
||||
DROP VIEW IF EXISTS v_pengurus_piket_hari_ini;
|
||||
DROP VIEW IF EXISTS v_prioritas_bantuan;
|
||||
DROP VIEW IF EXISTS v_rekap_bantuan;
|
||||
DROP VIEW IF EXISTS v_validasi_pendidikan;
|
||||
|
||||
-- ============================================================
|
||||
-- LANGKAH 2: BUAT ULANG SEMUA TABEL
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE NOT NULL,
|
||||
deskripsi TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE rumah_ibadah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
jenis ENUM('Masjid','Musholla','Gereja Protestan','Gereja Katolik','Pura','Vihara','Klenteng','Lainnya') NOT NULL,
|
||||
kontak VARCHAR(50), alamat TEXT, kelurahan VARCHAR(100), kecamatan VARCHAR(100),
|
||||
lat DECIMAL(11,8) NOT NULL, lng DECIMAL(11,8) NOT NULL,
|
||||
radius INT DEFAULT 500, foto VARCHAR(255), aktif TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100), password VARCHAR(255) NOT NULL,
|
||||
role_id INT NOT NULL DEFAULT 4, rumah_ibadah_id INT NULL,
|
||||
avatar VARCHAR(255), aktif TINYINT(1) DEFAULT 1,
|
||||
last_login TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id),
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE keluarga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
no_kk VARCHAR(20) UNIQUE NOT NULL, alamat TEXT NOT NULL,
|
||||
rt VARCHAR(5), rw VARCHAR(5), kelurahan VARCHAR(100), kecamatan VARCHAR(100),
|
||||
kota VARCHAR(100) DEFAULT 'Pontianak', lat DECIMAL(11,8), lng DECIMAL(11,8),
|
||||
rumah_ibadah_id INT NULL,
|
||||
status_ekonomi ENUM('miskin','rentan','mampu') DEFAULT 'rentan',
|
||||
status_verifikasi ENUM('belum','terverifikasi','ditolak') DEFAULT 'belum',
|
||||
verified_by INT NULL, verified_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (verified_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE penduduk (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nik VARCHAR(16) UNIQUE NOT NULL, nama VARCHAR(150) NOT NULL,
|
||||
jenis_kelamin ENUM('L','P') NOT NULL, tanggal_lahir DATE NOT NULL,
|
||||
tempat_lahir VARCHAR(100),
|
||||
status_keluarga ENUM('kepala_keluarga','istri','anak','orang_tua','famili_lain','lainnya') DEFAULT 'anak',
|
||||
status_kawin ENUM('belum_kawin','kawin','cerai_hidup','cerai_mati') DEFAULT 'belum_kawin',
|
||||
status_hidup ENUM('hidup','meninggal') DEFAULT 'hidup',
|
||||
tanggal_meninggal DATE NULL, keluarga_id INT NULL,
|
||||
pendidikan_terakhir ENUM('tidak_sekolah','sd','smp','sma','smk','d1','d2','d3','s1','s2','s3') DEFAULT 'tidak_sekolah',
|
||||
status_pendidikan ENUM('tidak_sekolah','masih_sekolah','lulus','putus_sekolah') DEFAULT 'tidak_sekolah',
|
||||
pekerjaan VARCHAR(100), penghasilan DECIMAL(12,2) DEFAULT 0,
|
||||
status_ekonomi ENUM('miskin','rentan','mampu') DEFAULT 'rentan',
|
||||
kondisi_kesehatan ENUM('sehat','sakit_ringan','sakit_berat','disabilitas') DEFAULT 'sehat',
|
||||
catatan_kesehatan TEXT, domisili_sama_kk TINYINT(1) DEFAULT 1,
|
||||
alamat_domisili TEXT, lat DECIMAL(11,8), lng DECIMAL(11,8),
|
||||
foto VARCHAR(255),
|
||||
no_hp VARCHAR(20) NULL,
|
||||
kebutuhan_pelatihan TEXT NULL,
|
||||
prioritas_bantuan ENUM('darurat','tinggi','sedang','rendah') NULL,
|
||||
created_by INT NULL, updated_by INT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (keluarga_id) REFERENCES keluarga(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE riwayat_pendidikan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY, penduduk_id INT NOT NULL,
|
||||
jenis ENUM('formal','pelatihan','kursus') DEFAULT 'formal',
|
||||
nama_institusi VARCHAR(150), jenjang VARCHAR(50), jurusan VARCHAR(100),
|
||||
tahun_masuk YEAR, tahun_lulus YEAR NULL,
|
||||
status ENUM('lulus','putus','sedang_berjalan') DEFAULT 'lulus',
|
||||
sertifikat VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE jenis_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kode VARCHAR(20) UNIQUE NOT NULL, nama VARCHAR(100) NOT NULL,
|
||||
kategori ENUM('sembako','pendidikan','kesehatan','ekonomi','infrastruktur','lainnya') NOT NULL,
|
||||
satuan VARCHAR(50) DEFAULT 'paket', deskripsi TEXT,
|
||||
sumber_dana VARCHAR(100), aktif TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE program_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY, nama_program VARCHAR(150) NOT NULL,
|
||||
jenis_bantuan_id INT NOT NULL, anggaran DECIMAL(15,2) DEFAULT 0,
|
||||
target_penerima INT DEFAULT 0, tanggal_mulai DATE, tanggal_selesai DATE,
|
||||
status ENUM('perencanaan','berjalan','selesai','dibatalkan') DEFAULT 'perencanaan',
|
||||
penanggungjawab INT NULL, deskripsi TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (jenis_bantuan_id) REFERENCES jenis_bantuan(id),
|
||||
FOREIGN KEY (penanggungjawab) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE histori_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL, keluarga_id INT NULL,
|
||||
program_bantuan_id INT NULL, jenis_bantuan_id INT NOT NULL,
|
||||
jumlah DECIMAL(10,2) DEFAULT 1, satuan VARCHAR(50),
|
||||
nilai_rupiah DECIMAL(12,2) DEFAULT 0, tanggal_penyaluran DATE NOT NULL,
|
||||
status_penyaluran ENUM('dijadwalkan','disalurkan','gagal','dikembalikan') DEFAULT 'dijadwalkan',
|
||||
disalurkan_oleh INT NULL, rumah_ibadah_id INT NULL,
|
||||
bukti_foto VARCHAR(255), catatan TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (keluarga_id) REFERENCES keluarga(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (program_bantuan_id) REFERENCES program_bantuan(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (jenis_bantuan_id) REFERENCES jenis_bantuan(id),
|
||||
FOREIGN KEY (disalurkan_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE laporan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kode_laporan VARCHAR(20) UNIQUE, nama_pelapor VARCHAR(100),
|
||||
no_hp_pelapor VARCHAR(20), penduduk_id INT NULL,
|
||||
nama_terlapor VARCHAR(150), alamat_kejadian TEXT,
|
||||
lat DECIMAL(11,8), lng DECIMAL(11,8), deskripsi TEXT NOT NULL,
|
||||
kategori ENUM('kemiskinan','kesehatan','bencana','kekerasan','pendidikan','lainnya') DEFAULT 'kemiskinan',
|
||||
urgensi ENUM('rendah','sedang','tinggi','darurat') DEFAULT 'sedang',
|
||||
foto VARCHAR(255),
|
||||
status ENUM('pending','diverifikasi','diproses','selesai','ditolak') DEFAULT 'pending',
|
||||
diverifikasi_oleh INT NULL, diverifikasi_at TIMESTAMP NULL,
|
||||
catatan_verifikasi TEXT, diproses_oleh INT NULL, diproses_at TIMESTAMP NULL,
|
||||
diselesaikan_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (diverifikasi_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (diproses_oleh) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE pesan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
pengirim_id INT NOT NULL, penerima_id INT NULL,
|
||||
rumah_ibadah_id INT NULL, isi TEXT NOT NULL,
|
||||
tipe ENUM('teks','gambar','file') DEFAULT 'teks',
|
||||
lampiran VARCHAR(255), dibaca TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (pengirim_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (penerima_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE log_aktivitas (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NULL, aksi VARCHAR(100) NOT NULL,
|
||||
tabel_target VARCHAR(50), id_target INT NULL,
|
||||
data_lama JSON NULL, data_baru JSON NULL,
|
||||
ip_address VARCHAR(45), user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE notifikasi (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL, judul VARCHAR(150) NOT NULL,
|
||||
isi TEXT, tipe ENUM('info','warning','success','danger') DEFAULT 'info',
|
||||
url_target VARCHAR(255), dibaca TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE peristiwa_kependudukan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL, keluarga_id INT NULL,
|
||||
jenis ENUM('lahir','meninggal','menikah','cerai','pindah_masuk','pindah_keluar','ganti_nama','perubahan_status_ekonomi') NOT NULL,
|
||||
tanggal DATE NOT NULL, keterangan TEXT,
|
||||
alamat_asal TEXT NULL, alamat_tujuan TEXT NULL,
|
||||
sebab_meninggal VARCHAR(150) NULL, dokumen VARCHAR(255) NULL,
|
||||
dicatat_oleh INT NULL, diverifikasi TINYINT(1) DEFAULT 0,
|
||||
diverifikasi_oleh INT NULL, diverifikasi_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (keluarga_id) REFERENCES keluarga(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (dicatat_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (diverifikasi_oleh) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE rekomendasi_pelatihan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY, nama VARCHAR(100) NOT NULL,
|
||||
kategori ENUM('vokasi','kewirausahaan','digital','pertanian','kesehatan','pendidikan','lainnya') NOT NULL,
|
||||
deskripsi TEXT, penyelenggara VARCHAR(150), kontak_penyelenggara VARCHAR(100),
|
||||
usia_min INT DEFAULT 15, usia_maks INT DEFAULT 60,
|
||||
gender ENUM('L','P','semua') DEFAULT 'semua',
|
||||
status_ekonomi_sasaran ENUM('miskin','rentan','semua') DEFAULT 'semua',
|
||||
prasyarat TEXT, durasi VARCHAR(50), biaya DECIMAL(10,2) DEFAULT 0,
|
||||
aktif TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE penduduk_pelatihan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL, rekomendasi_pelatihan_id INT NOT NULL,
|
||||
status ENUM('direkomendasikan','mendaftar','sedang_berjalan','selesai','batal') DEFAULT 'direkomendasikan',
|
||||
tanggal_rekomendasi DATE NOT NULL, tanggal_mulai DATE NULL, tanggal_selesai DATE NULL,
|
||||
sertifikat VARCHAR(255) NULL, catatan TEXT, direkomendasikan_oleh INT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (rekomendasi_pelatihan_id) REFERENCES rekomendasi_pelatihan(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (direkomendasikan_oleh) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE jadwal_piket (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL, rumah_ibadah_id INT NOT NULL,
|
||||
tanggal DATE NOT NULL, shift ENUM('pagi','siang','sore','malam','full_day') DEFAULT 'full_day',
|
||||
jam_mulai TIME DEFAULT '08:00:00', jam_selesai TIME DEFAULT '17:00:00',
|
||||
keterangan TEXT,
|
||||
status ENUM('terjadwal','hadir','tidak_hadir','digantikan') DEFAULT 'terjadwal',
|
||||
digantikan_oleh INT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (digantikan_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
UNIQUE KEY unique_piket (user_id, tanggal, shift)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- LANGKAH 3: VIEWS
|
||||
-- ============================================================
|
||||
|
||||
CREATE OR REPLACE VIEW v_penduduk AS
|
||||
SELECT p.*, TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
k.no_kk, k.alamat AS alamat_kk, k.rt, k.rw, k.kelurahan, k.kecamatan,
|
||||
k.lat AS lat_kk, k.lng AS lng_kk, k.status_ekonomi AS status_ekonomi_kk,
|
||||
ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id=p.id AND hb.status_penyaluran='disalurkan') AS total_bantuan_diterima,
|
||||
(SELECT MAX(hb2.tanggal_penyaluran) FROM histori_bantuan hb2 WHERE hb2.penduduk_id=p.id) AS terakhir_dibantu
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id;
|
||||
|
||||
CREATE OR REPLACE VIEW v_prioritas_bantuan AS
|
||||
SELECT p.id, p.nik, p.nama, TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
p.status_ekonomi, p.kondisi_kesehatan, k.alamat, k.kelurahan, k.kecamatan, k.lat, k.lng,
|
||||
ri.nama AS nama_ibadah,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id=p.id AND hb.status_penyaluran='disalurkan') AS total_bantuan,
|
||||
CASE
|
||||
WHEN p.status_ekonomi='miskin' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'DARURAT'
|
||||
WHEN p.status_ekonomi='miskin' THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi='rentan' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi='rentan' THEN 'SEDANG'
|
||||
ELSE 'RENDAH'
|
||||
END AS prioritas
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id
|
||||
WHERE p.status_hidup='hidup'
|
||||
ORDER BY FIELD(p.status_ekonomi,'miskin','rentan','mampu'), FIELD(p.kondisi_kesehatan,'sakit_berat','disabilitas','sakit_ringan','sehat');
|
||||
|
||||
CREATE OR REPLACE VIEW v_laporan_aktif AS
|
||||
SELECT l.*, p.nama AS nama_penduduk_terdaftar, p.kondisi_kesehatan,
|
||||
TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur_penduduk,
|
||||
k.alamat AS alamat_kk, ri.nama AS nama_ibadah_terdekat,
|
||||
TIMESTAMPDIFF(HOUR,l.created_at,NOW()) AS jam_sejak_laporan
|
||||
FROM laporan l
|
||||
LEFT JOIN penduduk p ON l.penduduk_id=p.id
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id
|
||||
WHERE l.status NOT IN ('selesai','ditolak')
|
||||
ORDER BY FIELD(l.urgensi,'darurat','tinggi','sedang','rendah'), l.created_at ASC;
|
||||
|
||||
CREATE OR REPLACE VIEW v_rekap_bantuan AS
|
||||
SELECT p.id AS penduduk_id, p.nik, p.nama,
|
||||
TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
p.status_ekonomi, p.prioritas_bantuan, p.kondisi_kesehatan,
|
||||
k.no_kk, k.alamat, k.kelurahan, k.kecamatan, ri.nama AS nama_ibadah,
|
||||
COUNT(hb.id) AS total_transaksi_bantuan,
|
||||
SUM(CASE WHEN hb.status_penyaluran='disalurkan' THEN 1 ELSE 0 END) AS total_tersalurkan,
|
||||
SUM(CASE WHEN hb.status_penyaluran='disalurkan' THEN hb.nilai_rupiah ELSE 0 END) AS total_nilai_rupiah,
|
||||
MAX(hb.tanggal_penyaluran) AS terakhir_dibantu,
|
||||
MAX(CASE WHEN hb.status_penyaluran='disalurkan' AND YEAR(hb.tanggal_penyaluran)=YEAR(CURDATE()) AND MONTH(hb.tanggal_penyaluran)=MONTH(CURDATE()) THEN 1 ELSE 0 END) AS sudah_dibantu_bulan_ini,
|
||||
GROUP_CONCAT(DISTINCT CASE WHEN hb.status_penyaluran='disalurkan' AND YEAR(hb.tanggal_penyaluran)=YEAR(CURDATE()) AND MONTH(hb.tanggal_penyaluran)=MONTH(CURDATE()) THEN jb.nama ELSE NULL END ORDER BY jb.nama SEPARATOR ', ') AS bantuan_bulan_ini
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id
|
||||
LEFT JOIN histori_bantuan hb ON hb.penduduk_id=p.id
|
||||
LEFT JOIN jenis_bantuan jb ON hb.jenis_bantuan_id=jb.id
|
||||
WHERE p.status_hidup='hidup'
|
||||
GROUP BY p.id,p.nik,p.nama,p.tanggal_lahir,p.status_ekonomi,p.prioritas_bantuan,p.kondisi_kesehatan,k.no_kk,k.alamat,k.kelurahan,k.kecamatan,ri.nama
|
||||
ORDER BY FIELD(p.prioritas_bantuan,'darurat','tinggi','sedang','rendah'), terakhir_dibantu ASC;
|
||||
|
||||
CREATE OR REPLACE VIEW v_validasi_pendidikan AS
|
||||
SELECT p.id, p.nik, p.nama, TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
p.pendidikan_terakhir, p.status_pendidikan,
|
||||
CASE
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) < 6 AND p.status_pendidikan='masih_sekolah' THEN 'Terlalu muda untuk sekolah (< 6 tahun)'
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) BETWEEN 6 AND 12 AND p.pendidikan_terakhir NOT IN ('tidak_sekolah','sd') THEN 'Umur 6-12 tahun seharusnya max SD'
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) BETWEEN 13 AND 15 AND p.pendidikan_terakhir IN ('sma','smk','d1','d2','d3','s1','s2','s3') THEN 'Umur 13-15 tahun tidak mungkin SMA/kuliah'
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) < 18 AND p.pendidikan_terakhir IN ('d1','d2','d3','s1','s2','s3') THEN 'Di bawah 18 tahun tidak mungkin kuliah'
|
||||
ELSE NULL
|
||||
END AS peringatan_inkonsistensi,
|
||||
CASE
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) BETWEEN 18 AND 55 AND p.status_pendidikan IN ('tidak_sekolah','putus_sekolah') AND p.status_ekonomi IN ('miskin','rentan') THEN 'Rekomendasikan pelatihan vokasi atau kejar paket'
|
||||
WHEN TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) < 18 AND p.status_pendidikan IN ('tidak_sekolah','putus_sekolah') AND p.status_ekonomi IN ('miskin','rentan') THEN 'Rekomendasikan beasiswa / program kembali sekolah'
|
||||
ELSE NULL
|
||||
END AS saran_tindak_lanjut
|
||||
FROM penduduk p WHERE p.status_hidup='hidup'
|
||||
ORDER BY peringatan_inkonsistensi IS NULL, p.id;
|
||||
|
||||
CREATE OR REPLACE VIEW v_pengurus_piket_hari_ini AS
|
||||
SELECT jp.id AS jadwal_id, u.id AS user_id, u.nama AS nama_pengurus, u.email,
|
||||
ri.id AS rumah_ibadah_id, ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
|
||||
jp.shift, jp.jam_mulai, jp.jam_selesai, jp.status AS status_piket
|
||||
FROM jadwal_piket jp
|
||||
JOIN users u ON jp.user_id=u.id
|
||||
JOIN rumah_ibadah ri ON jp.rumah_ibadah_id=ri.id
|
||||
WHERE jp.tanggal=CURDATE() AND jp.status IN ('terjadwal','hadir') AND u.aktif=1;
|
||||
|
||||
-- ============================================================
|
||||
-- LANGKAH 4: TRIGGERS
|
||||
-- ============================================================
|
||||
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_penduduk_insert BEFORE INSERT ON penduduk FOR EACH ROW BEGIN
|
||||
SET NEW.prioritas_bantuan = CASE WHEN NEW.status_ekonomi='miskin' AND NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'darurat' WHEN NEW.status_ekonomi='miskin' THEN 'tinggi' WHEN NEW.status_ekonomi='rentan' AND NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'tinggi' WHEN NEW.status_ekonomi='rentan' THEN 'sedang' ELSE 'rendah' END;
|
||||
SET @umur = TIMESTAMPDIFF(YEAR, NEW.tanggal_lahir, CURDATE());
|
||||
SET NEW.kebutuhan_pelatihan = CASE WHEN @umur < 18 AND NEW.status_ekonomi IN ('miskin','rentan') THEN JSON_ARRAY('Beasiswa pendidikan','Bantuan seragam/buku') WHEN @umur BETWEEN 18 AND 35 AND NEW.status_ekonomi='miskin' THEN JSON_ARRAY('Pelatihan vokasi (jahit/las/bengkel)','Modal usaha UMKM','Literasi digital') WHEN @umur BETWEEN 18 AND 35 AND NEW.status_ekonomi='rentan' THEN JSON_ARRAY('Modal usaha UMKM','Pelatihan kewirausahaan') WHEN @umur BETWEEN 36 AND 55 AND NEW.status_ekonomi='miskin' AND NEW.kondisi_kesehatan NOT IN ('sakit_berat','disabilitas') THEN JSON_ARRAY('Pelatihan pertanian/budidaya','Modal usaha UMKM') WHEN NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN JSON_ARRAY('Bantuan kesehatan gratis','Alat bantu disabilitas','Pendampingan sosial') WHEN @umur > 55 AND NEW.status_ekonomi IN ('miskin','rentan') THEN JSON_ARRAY('Bantuan sembako rutin','Pendampingan lansia','Bantuan kesehatan') ELSE NULL END;
|
||||
END$$
|
||||
CREATE TRIGGER trg_penduduk_update BEFORE UPDATE ON penduduk FOR EACH ROW BEGIN
|
||||
SET NEW.prioritas_bantuan = CASE WHEN NEW.status_ekonomi='miskin' AND NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'darurat' WHEN NEW.status_ekonomi='miskin' THEN 'tinggi' WHEN NEW.status_ekonomi='rentan' AND NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'tinggi' WHEN NEW.status_ekonomi='rentan' THEN 'sedang' ELSE 'rendah' END;
|
||||
SET @umur = TIMESTAMPDIFF(YEAR, NEW.tanggal_lahir, CURDATE());
|
||||
SET NEW.kebutuhan_pelatihan = CASE WHEN @umur < 18 AND NEW.status_ekonomi IN ('miskin','rentan') THEN JSON_ARRAY('Beasiswa pendidikan','Bantuan seragam/buku') WHEN @umur BETWEEN 18 AND 35 AND NEW.status_ekonomi='miskin' THEN JSON_ARRAY('Pelatihan vokasi (jahit/las/bengkel)','Modal usaha UMKM','Literasi digital') WHEN @umur BETWEEN 18 AND 35 AND NEW.status_ekonomi='rentan' THEN JSON_ARRAY('Modal usaha UMKM','Pelatihan kewirausahaan') WHEN @umur BETWEEN 36 AND 55 AND NEW.status_ekonomi='miskin' AND NEW.kondisi_kesehatan NOT IN ('sakit_berat','disabilitas') THEN JSON_ARRAY('Pelatihan pertanian/budidaya','Modal usaha UMKM') WHEN NEW.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN JSON_ARRAY('Bantuan kesehatan gratis','Alat bantu disabilitas','Pendampingan sosial') WHEN @umur > 55 AND NEW.status_ekonomi IN ('miskin','rentan') THEN JSON_ARRAY('Bantuan sembako rutin','Pendampingan lansia','Bantuan kesehatan') ELSE NULL END;
|
||||
END$$
|
||||
CREATE TRIGGER trg_penduduk_meninggal AFTER UPDATE ON penduduk FOR EACH ROW BEGIN
|
||||
IF OLD.status_hidup='hidup' AND NEW.status_hidup='meninggal' THEN
|
||||
INSERT INTO peristiwa_kependudukan (penduduk_id,keluarga_id,jenis,tanggal,keterangan,dicatat_oleh) VALUES (NEW.id,NEW.keluarga_id,'meninggal',IFNULL(NEW.tanggal_meninggal,CURDATE()),'Dicatat otomatis dari update status_hidup',NEW.updated_by);
|
||||
END IF;
|
||||
END$$
|
||||
CREATE TRIGGER trg_penduduk_kawin AFTER UPDATE ON penduduk FOR EACH ROW BEGIN
|
||||
IF OLD.status_kawin != 'kawin' AND NEW.status_kawin = 'kawin' THEN
|
||||
INSERT INTO peristiwa_kependudukan (penduduk_id,keluarga_id,jenis,tanggal,keterangan,dicatat_oleh) VALUES (NEW.id,NEW.keluarga_id,'menikah',CURDATE(),'Dicatat otomatis dari update status_kawin',NEW.updated_by);
|
||||
END IF;
|
||||
IF OLD.status_kawin='kawin' AND NEW.status_kawin IN ('cerai_hidup','cerai_mati') THEN
|
||||
INSERT INTO peristiwa_kependudukan (penduduk_id,keluarga_id,jenis,tanggal,keterangan,dicatat_oleh) VALUES (NEW.id,NEW.keluarga_id,'cerai',CURDATE(),CONCAT('Status kawin berubah ke: ',NEW.status_kawin),NEW.updated_by);
|
||||
END IF;
|
||||
END$$
|
||||
CREATE TRIGGER trg_laporan_notifikasi AFTER INSERT ON laporan FOR EACH ROW BEGIN
|
||||
DECLARE done INT DEFAULT FALSE; DECLARE v_user_id INT;
|
||||
DECLARE cur CURSOR FOR SELECT u.id FROM users u WHERE u.role_id=2 AND u.aktif=1;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=TRUE;
|
||||
IF NEW.urgensi IN ('darurat','tinggi') THEN
|
||||
OPEN cur; read_loop: LOOP FETCH cur INTO v_user_id; IF done THEN LEAVE read_loop; END IF;
|
||||
INSERT INTO notifikasi (user_id,judul,isi,tipe,url_target) VALUES (v_user_id,CONCAT('[',UPPER(NEW.urgensi),'] Laporan baru masuk'),CONCAT('Kode: ',NEW.kode_laporan,' | Kategori: ',NEW.kategori,' | Alamat: ',IFNULL(NEW.alamat_kejadian,'-')),CASE WHEN NEW.urgensi='darurat' THEN 'danger' ELSE 'warning' END,CONCAT('/laporan.php?id=',NEW.id));
|
||||
END LOOP; CLOSE cur;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- ============================================================
|
||||
-- LANGKAH 5: DATA AWAL (roles + users + data dummy)
|
||||
-- ============================================================
|
||||
|
||||
INSERT IGNORE INTO roles (nama, slug, deskripsi) VALUES
|
||||
('Administrator','admin','Akses penuh ke seluruh sistem'),
|
||||
('Pengurus Rumah Ibadah','pengurus','Input data, verifikasi laporan, kelola bantuan'),
|
||||
('Pimpinan Daerah','pimpinan','Monitoring & laporan (read-only)'),
|
||||
('Masyarakat','masyarakat','Lihat data terbatas & buat laporan');
|
||||
|
||||
INSERT IGNORE INTO rumah_ibadah (nama,jenis,kontak,alamat,kelurahan,kecamatan,lat,lng,radius) VALUES
|
||||
('Masjid Raya Mujahiddin','Masjid','0561-748551','Jl. Ahmad Yani, Pontianak Selatan','Akcaya','Pontianak Selatan',-0.026300,109.342500,600),
|
||||
('Masjid Jami Pontianak','Masjid','0561-734567','Jl. Tanjungpura No. 1','Dalam Bugis','Pontianak Timur',-0.018500,109.331500,500),
|
||||
('Gereja Katedral St. Yosef','Gereja Katolik','0561-712233','Jl. Rahadi Usman No. 3','Tengah','Pontianak Kota',-0.021400,109.329800,400),
|
||||
('Vihara Tri Dharma Bumi Raya','Vihara','0561-745566','Jl. Diponegoro No. 45','Sungai Bangkong','Pontianak Kota',-0.030200,109.335800,350),
|
||||
('Masjid Al-Falah','Masjid','0561-891234','Jl. Prof. M. Yamin No. 7','Sungai Jawi','Pontianak Barat',-0.011200,109.320500,450),
|
||||
('Gereja Bethel Indonesia','Gereja Protestan','0561-723456','Jl. Imam Bonjol No. 15','Benua Melayu Darat','Pontianak Selatan',-0.033100,109.337200,380);
|
||||
|
||||
-- Password semua akun: password123
|
||||
INSERT IGNORE INTO users (nama,username,email,password,role_id,rumah_ibadah_id) VALUES
|
||||
('Administrator Sistem','admin','admin@webgis.id','$2y$10$TKh8H1.PpuAuGAQ1qwLSJ.Q7hV5jB87dJ3y/N6OvHT9Lq.LdLFOiO',1,NULL),
|
||||
('H. Ruslan Pengurus','pengurus1','ruslan@webgis.id','$2y$10$TKh8H1.PpuAuGAQ1qwLSJ.Q7hV5jB87dJ3y/N6OvHT9Lq.LdLFOiO',2,1),
|
||||
('Pdt. Stefanus','pengurus2','stefanus@webgis.id','$2y$10$TKh8H1.PpuAuGAQ1qwLSJ.Q7hV5jB87dJ3y/N6OvHT9Lq.LdLFOiO',2,3),
|
||||
('Bapak Walikota','pimpinan','walikota@pontianak.go.id','$2y$10$TKh8H1.PpuAuGAQ1qwLSJ.Q7hV5jB87dJ3y/N6OvHT9Lq.LdLFOiO',3,NULL),
|
||||
('Warga Biasa','warga','warga@gmail.com','$2y$10$TKh8H1.PpuAuGAQ1qwLSJ.Q7hV5jB87dJ3y/N6OvHT9Lq.LdLFOiO',4,NULL);
|
||||
|
||||
INSERT IGNORE INTO jenis_bantuan (kode,nama,kategori,satuan,deskripsi,sumber_dana) VALUES
|
||||
('SMKO-001','Paket Sembako Bulanan','sembako','paket','Beras 5kg, minyak, gula, telur','APBD Kota'),
|
||||
('PKSH-001','Bantuan Kesehatan Gratis','kesehatan','kartu','Kartu berobat gratis Puskesmas','APBN (JKN)'),
|
||||
('PDDK-001','Beasiswa Pendidikan SD-SMA','pendidikan','tahun','Bantuan biaya pendidikan Rp 300ribu/bulan','APBD Provinsi'),
|
||||
('EKON-001','Modal Usaha UMKM','ekonomi','paket','Modal awal usaha kecil Rp 1-5 juta','Dana CSR'),
|
||||
('SMKO-002','Paket Lebaran','sembako','paket','Paket sembako + busana hari raya','Donasi Masyarakat');
|
||||
|
||||
INSERT IGNORE INTO rekomendasi_pelatihan (nama,kategori,deskripsi,penyelenggara,kontak_penyelenggara,usia_min,usia_maks,gender,status_ekonomi_sasaran,prasyarat,durasi,biaya) VALUES
|
||||
('Pelatihan Ojek Online','vokasi','Cara daftar & operasikan aplikasi ojek online','Balai Latihan Kerja Pontianak','0561-891234',18,50,'L','semua','Punya motor & SIM C','3 hari',0),
|
||||
('Menjahit & Konveksi','vokasi','Menjahit pakaian dasar hingga konveksi sederhana','Dinas Koperasi & UKM Kota Pontianak','0561-737777',17,55,'P','miskin',NULL,'2 minggu',0),
|
||||
('Modal Usaha Warung/UMKM','kewirausahaan','Bantuan modal Rp1-5 juta + pelatihan manajemen keuangan','Baznas Kota Pontianak','0561-745678',20,60,'semua','miskin',NULL,'1 hari orientasi',0),
|
||||
('Literasi Digital & E-commerce','digital','Jualan online lewat Shopee/Tokopedia','Relawan TIK Kalimantan Barat','081234500000',17,40,'semua','semua','Punya smartphone','2 hari',0),
|
||||
('Kejar Paket B/C (setara SMP/SMA)','pendidikan','Pendidikan kesetaraan bagi yang tidak sempat selesaikan SMP/SMA','PKBM Harapan Bangsa Pontianak','081211223344',18,45,'semua','semua',NULL,'6 bulan',0);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ============================================================
|
||||
-- SELESAI! Login dengan: admin / password123
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- DATA DUMMY LENGKAP — Keluarga, Penduduk, Bantuan, Laporan
|
||||
-- ============================================================
|
||||
USE webgisuas;
|
||||
|
||||
INSERT IGNORE INTO keluarga (no_kk,alamat,rt,rw,kelurahan,kecamatan,lat,lng,rumah_ibadah_id,status_ekonomi,status_verifikasi,verified_by) VALUES
|
||||
('6171012501800001','Jl. Sei Raya Dalam No. 5','002','003','Akcaya','Pontianak Selatan',-0.027100,109.344000,1,'miskin','terverifikasi',2),
|
||||
('6171012501850002','Jl. Gusti Hamzah No. 12','001','001','Dalam Bugis','Pontianak Timur',-0.019500,109.332000,2,'miskin','terverifikasi',2),
|
||||
('6171012501900003','Jl. Purnama No. 8','003','002','Sungai Bangkong','Pontianak Kota',-0.028000,109.337000,1,'rentan','terverifikasi',2),
|
||||
('6171012501750004','Jl. Putri Candramidi No. 3','001','001','Tengah','Pontianak Kota',-0.022000,109.330500,3,'miskin','terverifikasi',3),
|
||||
('6171012501820005','Jl. Gajah Mada No. 22','004','002','Sungai Bangkong','Pontianak Kota',-0.031000,109.336500,4,'rentan','terverifikasi',2),
|
||||
('6171012501960006','Jl. Tanjung Pura Gang IV','002','001','Sungai Jawi','Pontianak Barat',-0.012000,109.321000,5,'mampu','terverifikasi',2),
|
||||
('6171012501700007','Jl. Imam Bonjol Gg. Kenari No. 4','003','003','Benua Melayu Darat','Pontianak Selatan',-0.034000,109.338000,6,'miskin','belum',NULL);
|
||||
|
||||
INSERT IGNORE INTO penduduk (nik,nama,jenis_kelamin,tanggal_lahir,tempat_lahir,status_keluarga,status_kawin,keluarga_id,pendidikan_terakhir,status_pendidikan,pekerjaan,penghasilan,status_ekonomi,kondisi_kesehatan,created_by) VALUES
|
||||
('6171010101800001','Budi Santoso','L','1980-03-15','Pontianak','kepala_keluarga','kawin',1,'sd','lulus','Buruh Harian',800000,'miskin','sehat',2),
|
||||
('6171010101850002','Dewi Santoso','P','1985-07-22','Sambas','istri','kawin',1,'sd','lulus','Ibu Rumah Tangga',0,'miskin','sehat',2),
|
||||
('6171010101100003','Rian Santoso','L','2010-01-10','Pontianak','anak','belum_kawin',1,'sd','masih_sekolah','Pelajar',0,'miskin','sehat',2),
|
||||
('6171010101850004','Siti Rahayu','P','1985-11-30','Mempawah','kepala_keluarga','cerai_mati',2,'smp','lulus','Pedagang Kecil',600000,'miskin','sakit_ringan',2),
|
||||
('6171010101120005','Anis Rahayu','P','2012-06-18','Pontianak','anak','belum_kawin',2,'sd','masih_sekolah','Pelajar',0,'miskin','sehat',2),
|
||||
('6171010101900006','Ahmad Fauzi','L','1990-09-05','Ketapang','kepala_keluarga','kawin',3,'sma','lulus','Ojek Online',1200000,'rentan','sehat',2),
|
||||
('6171010101920007','Nurhasanah','P','1992-04-12','Pontianak','istri','kawin',3,'smp','lulus','Ibu Rumah Tangga',0,'rentan','sehat',2),
|
||||
('6171010101750008','Maria Yuliani','P','1975-08-20','Sintang','kepala_keluarga','cerai_hidup',4,'sd','lulus','Pembantu Rumah Tangga',700000,'miskin','sakit_berat',3),
|
||||
('6171010101960009','Kevin Yuliani','L','2005-12-03','Pontianak','anak','belum_kawin',4,'smp','masih_sekolah','Pelajar',0,'miskin','sehat',3),
|
||||
('6171010101820010','Hendra Wijaya','L','1982-02-28','Pontianak','kepala_keluarga','kawin',5,'sma','lulus','Nelayan',1500000,'rentan','sehat',2),
|
||||
('6171010101960011','Putri Lestari','P','1996-05-14','Pontianak','kepala_keluarga','belum_kawin',6,'s1','lulus','Guru Honor',2500000,'mampu','sehat',2),
|
||||
('6171010101700012','Pak Slamet Riyadi','L','1970-11-11','Jogja','kepala_keluarga','kawin',7,'sd','lulus','Pengangguran',0,'miskin','disabilitas',NULL);
|
||||
|
||||
INSERT IGNORE INTO program_bantuan (nama_program,jenis_bantuan_id,anggaran,target_penerima,tanggal_mulai,tanggal_selesai,status,penanggungjawab) VALUES
|
||||
('Bantuan Sembako Ramadan 2025',1,50000000,200,'2025-03-01','2025-04-30','selesai',2),
|
||||
('JKN Gratis Warga Miskin Q1 2025',2,0,500,'2025-01-01','2025-03-31','selesai',2),
|
||||
('Beasiswa 2025',3,30000000,50,'2025-01-01','2025-12-31','berjalan',2);
|
||||
|
||||
INSERT IGNORE INTO histori_bantuan (penduduk_id,keluarga_id,program_bantuan_id,jenis_bantuan_id,jumlah,satuan,nilai_rupiah,tanggal_penyaluran,status_penyaluran,disalurkan_oleh,rumah_ibadah_id) VALUES
|
||||
(1,1,1,1,1,'paket',250000,'2025-03-20','disalurkan',2,1),
|
||||
(2,1,1,1,1,'paket',250000,'2025-03-20','disalurkan',2,1),
|
||||
(4,2,1,1,1,'paket',250000,'2025-03-21','disalurkan',2,2),
|
||||
(8,4,1,1,1,'paket',250000,'2025-03-22','disalurkan',3,3),
|
||||
(10,5,1,1,1,'paket',250000,'2025-03-20','disalurkan',2,4),
|
||||
(1,1,2,2,1,'kartu',0,'2025-01-15','disalurkan',2,1),
|
||||
(4,2,2,2,1,'kartu',0,'2025-01-15','disalurkan',2,2),
|
||||
(8,4,2,2,1,'kartu',0,'2025-01-16','disalurkan',3,3),
|
||||
(3,1,3,3,1,'tahun',3600000,'2025-01-10','disalurkan',2,1),
|
||||
(5,2,3,3,1,'tahun',3600000,'2025-01-10','disalurkan',2,2);
|
||||
|
||||
INSERT IGNORE INTO laporan (kode_laporan,nama_pelapor,no_hp_pelapor,penduduk_id,deskripsi,kategori,urgensi,alamat_kejadian,lat,lng,status) VALUES
|
||||
('LPR-2025-001','Anonim',NULL,8,'Ibu Maria sedang sakit keras sudah 2 minggu tidak bisa berobat karena tidak punya biaya. Mohon segera dibantu.','kesehatan','darurat','Jl. Putri Candramidi No. 3',-0.022000,109.330500,'diproses'),
|
||||
('LPR-2025-002','Tetangga Pak Slamet','081234567890',12,'Pak Slamet penyandang disabilitas, tinggal sendirian, tidak ada yang urus dan butuh kursi roda.','kemiskinan','tinggi','Jl. Imam Bonjol Gg. Kenari No. 4',-0.034000,109.338000,'diverifikasi'),
|
||||
('LPR-2025-003',NULL,NULL,NULL,'Ada keluarga baru pindah ke gang belakang masjid, kondisinya sangat memprihatinkan, anak-anak tidak sekolah.','pendidikan','tinggi','Gang Belakang Masjid Al-Falah',-0.011500,109.321200,'pending');
|
||||
|
||||
INSERT IGNORE INTO riwayat_pendidikan (penduduk_id,jenis,nama_institusi,jenjang,tahun_masuk,tahun_lulus,status) VALUES
|
||||
(1,'formal','SDN 5 Pontianak','SD',1987,1993,'lulus'),
|
||||
(4,'formal','SDN 3 Mempawah','SD',1992,1998,'lulus'),
|
||||
(4,'formal','SMPN 2 Mempawah','SMP',1998,2001,'lulus'),
|
||||
(6,'formal','SDN 7 Ketapang','SD',1997,2003,'lulus'),
|
||||
(6,'formal','SMPN 1 Ketapang','SMP',2003,2006,'lulus'),
|
||||
(6,'formal','SMAN 1 Ketapang','SMA',2006,2009,'lulus'),
|
||||
(6,'pelatihan','Balai Latihan Kerja Pontianak','Pelatihan Ojek Online',2019,2019,'lulus'),
|
||||
(11,'formal','UNTAN Pontianak','S1',2014,2018,'lulus');
|
||||
|
||||
INSERT IGNORE INTO peristiwa_kependudukan (penduduk_id,keluarga_id,jenis,tanggal,keterangan,dicatat_oleh,diverifikasi) VALUES
|
||||
(1,1,'lahir','1980-03-15','Data historis dari KK',2,1),
|
||||
(4,2,'lahir','1985-11-30','Data historis dari KK',2,1),
|
||||
(4,2,'menikah','2008-06-01','Menikah, suami meninggal 2020',2,1),
|
||||
(8,4,'lahir','1975-08-20','Data historis dari KK',3,1),
|
||||
(8,4,'menikah','2000-03-10','Menikah',3,1),
|
||||
(8,4,'cerai','2015-07-22','Cerai hidup',3,1),
|
||||
(12,7,'pindah_masuk','2022-01-15','Pindah dari Yogyakarta',NULL,0);
|
||||
|
||||
INSERT IGNORE INTO jadwal_piket (user_id,rumah_ibadah_id,tanggal,shift,jam_mulai,jam_selesai) VALUES
|
||||
(2,1,CURDATE(),'pagi','08:00:00','12:00:00'),
|
||||
(2,1,DATE_ADD(CURDATE(),INTERVAL 1 DAY),'pagi','08:00:00','12:00:00'),
|
||||
(3,3,CURDATE(),'full_day','08:00:00','17:00:00');
|
||||
|
||||
INSERT IGNORE INTO log_aktivitas (user_id,aksi,tabel_target,id_target,ip_address) VALUES
|
||||
(2,'CREATE','penduduk',1,'127.0.0.1'),
|
||||
(2,'UPDATE_BANTUAN','histori_bantuan',1,'127.0.0.1'),
|
||||
(3,'VERIFY_LAPORAN','laporan',2,'127.0.0.1');
|
||||
|
||||
-- Update trigger untuk data lama
|
||||
UPDATE penduduk SET kondisi_kesehatan=kondisi_kesehatan WHERE id > 0;
|
||||
|
||||
-- ============================================================
|
||||
-- SELESAI! Login: admin / password123
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// fix_password.php
|
||||
// Letakkan file ini di folder webgis_v2/, lalu akses sekali di browser
|
||||
// Setelah berhasil, HAPUS file ini!
|
||||
|
||||
require_once 'koneksi.php';
|
||||
|
||||
$newHash = password_hash('password123', PASSWORD_DEFAULT);
|
||||
|
||||
$users = [
|
||||
['username' => 'admin', 'hash' => $newHash],
|
||||
['username' => 'pengurus1', 'hash' => $newHash],
|
||||
['username' => 'pengurus2', 'hash' => $newHash],
|
||||
['username' => 'pimpinan', 'hash' => $newHash],
|
||||
['username' => 'warga', 'hash' => $newHash],
|
||||
];
|
||||
|
||||
echo "<h2>Fix Password Users</h2><pre>";
|
||||
foreach ($users as $u) {
|
||||
$hash = $conn->real_escape_string($u['hash']);
|
||||
$uname = $u['username'];
|
||||
$res = $conn->query("UPDATE users SET password='$hash' WHERE username='$uname'");
|
||||
if ($res) {
|
||||
echo "✅ {$uname} — password berhasil direset ke: password123\n";
|
||||
} else {
|
||||
echo "❌ {$uname} — gagal: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
echo "</pre>";
|
||||
echo "<p style='color:red'><strong>⚠️ HAPUS file fix_password.php ini sekarang setelah berhasil!</strong></p>";
|
||||
echo "<p><a href='login.php'>→ Coba Login Sekarang</a></p>";
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// jadwal_piket.php — Atur jadwal pengurus harian per masjid
|
||||
// Akses: admin, pengurus
|
||||
// ============================================================
|
||||
require_once 'koneksi.php';
|
||||
requireRole('admin','pengurus');
|
||||
|
||||
$msg = ''; $err = '';
|
||||
$role = getRole();
|
||||
$myUid = currentUserId();
|
||||
$myRumahIbadahId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
// Jika session kosong, ambil dari database
|
||||
if (!$myRumahIbadahId && $role === 'pengurus') {
|
||||
$uRow = $conn->query("SELECT rumah_ibadah_id FROM users WHERE id=$myUid")->fetch_assoc();
|
||||
$myRumahIbadahId = $uRow['rumah_ibadah_id'] ?? null;
|
||||
if ($myRumahIbadahId) $_SESSION['rumah_ibadah_id'] = $myRumahIbadahId; // cache ke session
|
||||
}
|
||||
|
||||
// Pengurus hanya lihat & kelola jadwal di rumah ibadahnya sendiri
|
||||
$filterRumah = ($role === 'pengurus' && $myRumahIbadahId)
|
||||
? "AND jp.rumah_ibadah_id = $myRumahIbadahId" : '';
|
||||
$filterRumahMaster = ($role === 'pengurus' && $myRumahIbadahId)
|
||||
? "WHERE id = $myRumahIbadahId" : '';
|
||||
|
||||
// ---- HANDLE POST --------------------------------------------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$aksi = $_POST['aksi'] ?? '';
|
||||
|
||||
// Tambah jadwal baru
|
||||
if ($aksi === 'tambah') {
|
||||
$uid = intval($_POST['user_id']);
|
||||
$riId = intval($_POST['rumah_ibadah_id']);
|
||||
$tgl = sanitize($conn, $_POST['tanggal']);
|
||||
$shift = $_POST['shift'];
|
||||
$jMul = sanitize($conn, $_POST['jam_mulai']);
|
||||
$jSls = sanitize($conn, $_POST['jam_selesai']);
|
||||
$ket = sanitize($conn, trim($_POST['keterangan'] ?? ''));
|
||||
|
||||
// Jika pengurus tidak punya rumah_ibadah_id (dari session), ambil dari user record
|
||||
if (!$riId && $role === 'pengurus') {
|
||||
$uRow = $conn->query("SELECT rumah_ibadah_id FROM users WHERE id=$myUid")->fetch_assoc();
|
||||
$riId = intval($uRow['rumah_ibadah_id'] ?? 0);
|
||||
}
|
||||
|
||||
if (!$riId) {
|
||||
$err = 'Rumah ibadah tidak ditemukan. Pastikan akun pengurus sudah ditetapkan ke rumah ibadah.';
|
||||
} else {
|
||||
// Cek konflik jadwal (user + tanggal + shift sudah ada)
|
||||
$cek = $conn->query("SELECT id FROM jadwal_piket
|
||||
WHERE user_id=$uid AND tanggal='$tgl' AND shift='$shift'")->fetch_assoc();
|
||||
if ($cek) {
|
||||
$err = 'Pengurus tersebut sudah terjadwal di shift & tanggal yang sama.';
|
||||
} else {
|
||||
$conn->query("INSERT INTO jadwal_piket
|
||||
(user_id, rumah_ibadah_id, tanggal, shift, jam_mulai, jam_selesai, keterangan)
|
||||
VALUES ($uid, $riId, '$tgl', '$shift', '$jMul', '$jSls', '$ket')");
|
||||
logAktivitas($conn, 'CREATE_JADWAL_PIKET', 'jadwal_piket', $conn->insert_id);
|
||||
$msg = 'Jadwal piket berhasil ditambahkan.';
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk: tambah jadwal seminggu sekaligus
|
||||
} elseif ($aksi === 'bulk') {
|
||||
$uid = intval($_POST['user_id']);
|
||||
$riId = intval($_POST['rumah_ibadah_id']);
|
||||
// Jika pengurus tidak punya rumah_ibadah_id, ambil dari user record
|
||||
if (!$riId && $role === 'pengurus') {
|
||||
$uRow = $conn->query("SELECT rumah_ibadah_id FROM users WHERE id=$myUid")->fetch_assoc();
|
||||
$riId = intval($uRow['rumah_ibadah_id'] ?? 0);
|
||||
}
|
||||
$tglMul = $_POST['tanggal_mulai'];
|
||||
$tglSls = $_POST['tanggal_selesai'];
|
||||
$shift = $_POST['shift'];
|
||||
$jMul = sanitize($conn, $_POST['jam_mulai']);
|
||||
$jSls = sanitize($conn, $_POST['jam_selesai']);
|
||||
$hariAktif = $_POST['hari_aktif'] ?? [];
|
||||
|
||||
$cur = new DateTime($tglMul);
|
||||
$end = new DateTime($tglSls);
|
||||
$added = 0;
|
||||
$namaHari = ['0'=>'Minggu','1'=>'Senin','2'=>'Selasa','3'=>'Rabu','4'=>'Kamis','5'=>'Jumat','6'=>'Sabtu'];
|
||||
|
||||
while ($cur <= $end) {
|
||||
$dow = $cur->format('w'); // 0=Minggu
|
||||
if (in_array($dow, $hariAktif)) {
|
||||
$tgl = $cur->format('Y-m-d');
|
||||
$cek = $conn->query("SELECT id FROM jadwal_piket WHERE user_id=$uid AND tanggal='$tgl' AND shift='$shift'")->fetch_assoc();
|
||||
if (!$cek) {
|
||||
$conn->query("INSERT INTO jadwal_piket (user_id,rumah_ibadah_id,tanggal,shift,jam_mulai,jam_selesai)
|
||||
VALUES ($uid,$riId,'$tgl','$shift','$jMul','$jSls')");
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
$cur->modify('+1 day');
|
||||
}
|
||||
$msg = "$added jadwal berhasil dibuat.";
|
||||
|
||||
// Update status (hadir / tidak_hadir / digantikan)
|
||||
} elseif ($aksi === 'update_status') {
|
||||
$id = intval($_POST['id']);
|
||||
$status = $_POST['status'];
|
||||
$gantiId= intval($_POST['digantikan_oleh'] ?? 0) ?: 'NULL';
|
||||
$conn->query("UPDATE jadwal_piket SET status='$status', digantikan_oleh=$gantiId WHERE id=$id");
|
||||
logAktivitas($conn, 'UPDATE_JADWAL_PIKET', 'jadwal_piket', $id);
|
||||
$msg = 'Status piket diperbarui.';
|
||||
|
||||
// Hapus jadwal
|
||||
} elseif ($aksi === 'hapus') {
|
||||
$id = intval($_POST['id']);
|
||||
$conn->query("DELETE FROM jadwal_piket WHERE id=$id");
|
||||
logAktivitas($conn, 'DELETE_JADWAL_PIKET', 'jadwal_piket', $id);
|
||||
$msg = 'Jadwal berhasil dihapus.';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Load data ----------------------------------------------
|
||||
// Minggu yang ditampilkan
|
||||
$weekOffset = intval($_GET['week'] ?? 0);
|
||||
$senin = new DateTime();
|
||||
$senin->modify('monday this week');
|
||||
$senin->modify("$weekOffset weeks");
|
||||
$minggu = clone $senin;
|
||||
$minggu->modify('+6 days');
|
||||
|
||||
$tglMulai = $senin->format('Y-m-d');
|
||||
$tglAkhir = $minggu->format('Y-m-d');
|
||||
|
||||
// Jadwal minggu ini
|
||||
$jadwalMinggu = $conn->query("
|
||||
SELECT jp.*,
|
||||
u.nama AS nama_pengurus, u.email,
|
||||
ri.nama AS nama_ibadah, ri.jenis AS jenis_ibadah,
|
||||
ug.nama AS nama_pengganti
|
||||
FROM jadwal_piket jp
|
||||
JOIN users u ON jp.user_id = u.id
|
||||
JOIN rumah_ibadah ri ON jp.rumah_ibadah_id = ri.id
|
||||
LEFT JOIN users ug ON jp.digantikan_oleh = ug.id
|
||||
WHERE jp.tanggal BETWEEN '$tglMulai' AND '$tglAkhir'
|
||||
$filterRumah
|
||||
ORDER BY jp.tanggal, FIELD(jp.shift,'pagi','siang','sore','malam','full_day'), u.nama
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Hari ini: siapa yang piket
|
||||
$piketHariIni = $conn->query("
|
||||
SELECT jp.*, u.nama AS nama_pengurus, ri.nama AS nama_ibadah
|
||||
FROM jadwal_piket jp
|
||||
JOIN users u ON jp.user_id = u.id
|
||||
JOIN rumah_ibadah ri ON jp.rumah_ibadah_id = ri.id
|
||||
WHERE jp.tanggal = CURDATE() AND jp.status IN ('terjadwal','hadir')
|
||||
$filterRumah
|
||||
ORDER BY FIELD(jp.shift,'pagi','siang','sore','malam','full_day')
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Daftar pengurus untuk dropdown
|
||||
$pengurusList = $conn->query("
|
||||
SELECT u.id, u.nama, ri.nama AS ibadah, u.rumah_ibadah_id
|
||||
FROM users u
|
||||
LEFT JOIN rumah_ibadah ri ON u.rumah_ibadah_id = ri.id
|
||||
WHERE u.role_id = 2 AND u.aktif = 1
|
||||
" . ($role==='pengurus' && $myRumahIbadahId ? "AND u.rumah_ibadah_id=$myRumahIbadahId" : "")
|
||||
. " ORDER BY u.nama")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Daftar rumah ibadah
|
||||
$rumahIbadahList = $conn->query("SELECT id, nama, jenis FROM rumah_ibadah $filterRumahMaster ORDER BY nama")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$shiftLabel = ['pagi'=>'🌅 Pagi','siang'=>'☀️ Siang','sore'=>'🌆 Sore','malam'=>'🌙 Malam','full_day'=>'🕐 Full Day'];
|
||||
$statusLabel = ['terjadwal'=>'Terjadwal','hadir'=>'Hadir','tidak_hadir'=>'Tidak Hadir','digantikan'=>'Digantikan'];
|
||||
$statusColor = ['terjadwal'=>'var(--blue)','hadir'=>'var(--green)','tidak_hadir'=>'var(--red)','digantikan'=>'var(--yellow)'];
|
||||
|
||||
// Kelompokkan jadwal per tanggal
|
||||
$byDate = [];
|
||||
foreach ($jadwalMinggu as $j) {
|
||||
$byDate[$j['tanggal']][] = $j;
|
||||
}
|
||||
|
||||
$namaHariArr = ['Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Minggu'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Jadwal Piket — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;--border:#243044;--border2:#2f3f57;--text:#e8edf3;--muted:#6b7e96;--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(240,180,41,.12);color:var(--yellow);}
|
||||
.container{max-width:1100px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:6px;}
|
||||
|
||||
/* Banner piket hari ini */
|
||||
.today-banner{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:24px;}
|
||||
.today-title{font-family:var(--font-head);font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;}
|
||||
.piket-cards{display:flex;gap:10px;flex-wrap:wrap;}
|
||||
.piket-card{background:var(--surface2);border:1px solid var(--border);border-radius:12px;padding:12px 16px;min-width:180px;}
|
||||
.piket-nama{font-weight:600;font-size:14px;margin-bottom:3px;}
|
||||
.piket-ibadah{font-size:11px;color:var(--muted);}
|
||||
.piket-shift{font-size:11px;margin-top:5px;}
|
||||
.empty-today{color:var(--muted);font-size:13px;padding:8px 0;}
|
||||
|
||||
/* Nav minggu */
|
||||
.week-nav{display:flex;align-items:center;gap:12px;margin-bottom:20px;}
|
||||
.week-label{font-family:var(--font-head);font-weight:700;font-size:16px;flex:1;text-align:center;}
|
||||
.btn{padding:9px 18px;border:none;border-radius:10px;font-family:var(--font-body);font-size:13px;font-weight:600;cursor:pointer;transition:opacity .2s;}
|
||||
.btn:hover{opacity:.88;}
|
||||
.btn-primary{background:var(--yellow);color:#111;}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
|
||||
.btn-sm{padding:5px 12px;font-size:12px;border-radius:8px;}
|
||||
.btn-success{background:var(--green);color:#fff;}
|
||||
.btn-danger{background:var(--red);color:#fff;}
|
||||
.btn-info{background:var(--blue);color:#fff;}
|
||||
|
||||
/* Grid kalender */
|
||||
.week-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:28px;}
|
||||
.day-col{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px;min-height:160px;}
|
||||
.day-col.today{border-color:var(--yellow);background:rgba(240,180,41,.05);}
|
||||
.day-header{font-family:var(--font-head);font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;margin-bottom:4px;}
|
||||
.day-date{font-family:var(--font-head);font-size:20px;font-weight:800;margin-bottom:10px;}
|
||||
.day-col.today .day-date{color:var(--yellow);}
|
||||
.piket-item{background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:7px 9px;margin-bottom:6px;font-size:11px;}
|
||||
.piket-item-name{font-weight:600;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.piket-item-meta{color:var(--muted);font-size:10px;}
|
||||
.piket-item-actions{display:flex;gap:4px;margin-top:5px;}
|
||||
.dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;vertical-align:middle;}
|
||||
|
||||
/* Modal */
|
||||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:500;align-items:center;justify-content:center;padding:20px;}
|
||||
.modal-bg.open{display:flex;}
|
||||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:20px;width:100%;max-width:560px;max-height:90vh;overflow-y:auto;}
|
||||
.modal-header{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);}
|
||||
.modal-title{font-family:var(--font-head);font-size:16px;font-weight:700;flex:1;}
|
||||
.modal-close{width:30px;height:30px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);font-size:16px;}
|
||||
.modal-body{padding:22px;}
|
||||
.tabs-modal{display:flex;gap:4px;margin-bottom:20px;background:var(--surface2);border-radius:10px;padding:4px;}
|
||||
.tab-modal-btn{flex:1;padding:8px;border:none;background:transparent;color:var(--muted);font-family:var(--font-body);font-size:12px;font-weight:600;cursor:pointer;border-radius:8px;transition:all .15s;}
|
||||
.tab-modal-btn.active{background:var(--surface);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.3);}
|
||||
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
.form-full{grid-column:1/-1;}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;}
|
||||
.form-group label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:500;}
|
||||
.form-group input,.form-group select{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 13px;color:var(--text);font-family:var(--font-body);font-size:13.5px;outline:none;transition:border-color .2s;width:100%;}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--yellow);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
.hari-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;}
|
||||
.hari-opt{display:none;}
|
||||
.hari-label{display:flex;align-items:center;justify-content:center;padding:8px 4px;border:1px solid var(--border);border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;transition:all .15s;text-align:center;}
|
||||
.hari-opt:checked + .hari-label{border-color:var(--yellow);background:rgba(240,180,41,.15);color:var(--yellow);}
|
||||
.form-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px;}
|
||||
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
||||
.toast{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 16px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideIn .2s ease;min-width:260px;}
|
||||
.toast.success{border-left:3px solid var(--green);}.toast.error{border-left:3px solid var(--red);}
|
||||
@keyframes slideIn{from{transform:translateX(20px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
.alert{padding:12px 16px;border-radius:10px;font-size:13px;margin-bottom:16px;}
|
||||
.alert-ok{background:rgba(39,174,114,.12);border:1px solid rgba(39,174,114,.3);color:var(--green);}
|
||||
.alert-err{background:rgba(232,80,80,.15);border:1px solid rgba(232,80,80,.3);color:var(--red);}
|
||||
@media(max-width:700px){.week-grid{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastCont"></div>
|
||||
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="laporan.php" class="nav-link">Laporan</a>
|
||||
<a href="jadwal_piket.php" class="nav-link active">Jadwal Piket</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted)">👤 <?= htmlspecialchars($_SESSION['nama']) ?></div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>📅 Jadwal Piket Pengurus</h2>
|
||||
<p style="color:var(--muted);font-size:13px;margin-bottom:20px;">Atur pengurus yang bertugas setiap hari agar laporan warga selalu ada yang menangani.</p>
|
||||
|
||||
<?php if ($msg): ?><div class="alert alert-ok">✅ <?= htmlspecialchars($msg) ?></div><?php endif; ?>
|
||||
<?php if ($err): ?><div class="alert alert-err">⚠️ <?= htmlspecialchars($err) ?></div><?php endif; ?>
|
||||
|
||||
<!-- Banner: Piket Hari Ini -->
|
||||
<div class="today-banner">
|
||||
<div class="today-title">🟢 Pengurus Piket Hari Ini — <?= date('l, d F Y') ?></div>
|
||||
<?php if (empty($piketHariIni)): ?>
|
||||
<div class="empty-today">⚠️ Belum ada pengurus terjadwal hari ini. Segera tambahkan jadwal!</div>
|
||||
<?php else: ?>
|
||||
<div class="piket-cards">
|
||||
<?php foreach ($piketHariIni as $p): ?>
|
||||
<div class="piket-card">
|
||||
<div class="piket-nama"><?= htmlspecialchars($p['nama_pengurus']) ?></div>
|
||||
<div class="piket-ibadah">🕌 <?= htmlspecialchars($p['nama_ibadah']) ?></div>
|
||||
<div class="piket-shift">
|
||||
<span class="dot" style="background:var(--green)"></span>
|
||||
<?= $shiftLabel[$p['shift']] ?> · <?= substr($p['jam_mulai'],0,5) ?>–<?= substr($p['jam_selesai'],0,5) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Navigasi Minggu -->
|
||||
<div class="week-nav">
|
||||
<a href="?week=<?= $weekOffset-1 ?>" class="btn btn-outline">← Minggu Lalu</a>
|
||||
<div class="week-label">
|
||||
<?= $senin->format('d M') ?> – <?= $minggu->format('d M Y') ?>
|
||||
<?php if ($weekOffset===0): ?> <span style="font-size:12px;color:var(--yellow)">(Minggu ini)</span><?php endif; ?>
|
||||
</div>
|
||||
<a href="?week=<?= $weekOffset+1 ?>" class="btn btn-outline">Minggu Depan →</a>
|
||||
<button class="btn btn-primary" onclick="openModalTambah()">+ Tambah Jadwal</button>
|
||||
</div>
|
||||
|
||||
<!-- Grid Kalender Mingguan -->
|
||||
<div class="week-grid">
|
||||
<?php
|
||||
$curDay = clone $senin;
|
||||
$today = date('Y-m-d');
|
||||
for ($d = 0; $d < 7; $d++):
|
||||
$tglStr = $curDay->format('Y-m-d');
|
||||
$isToday = $tglStr === $today;
|
||||
$items = $byDate[$tglStr] ?? [];
|
||||
?>
|
||||
<div class="day-col <?= $isToday?'today':'' ?>">
|
||||
<div class="day-header"><?= $namaHariArr[$d] ?></div>
|
||||
<div class="day-date"><?= $curDay->format('d') ?></div>
|
||||
<?php if (empty($items)): ?>
|
||||
<div style="font-size:10px;color:var(--border2);text-align:center;padding-top:12px;">Kosong</div>
|
||||
<?php else: foreach ($items as $item): ?>
|
||||
<div class="piket-item">
|
||||
<div class="piket-item-name" title="<?= htmlspecialchars($item['nama_pengurus']) ?>">
|
||||
<span class="dot" style="background:<?= $statusColor[$item['status']] ?>"></span>
|
||||
<?= htmlspecialchars($item['nama_pengurus']) ?>
|
||||
</div>
|
||||
<div class="piket-item-meta">
|
||||
<?= $shiftLabel[$item['shift']] ?><br>
|
||||
<?= substr($item['jam_mulai'],0,5) ?>–<?= substr($item['jam_selesai'],0,5) ?><br>
|
||||
🕌 <?= htmlspecialchars($item['nama_ibadah']) ?>
|
||||
</div>
|
||||
<?php if ($item['digantikan_oleh']): ?>
|
||||
<div style="font-size:10px;color:var(--yellow);margin-top:3px">→ Diganti: <?= htmlspecialchars($item['nama_pengganti']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasRole('admin','pengurus')): ?>
|
||||
<div class="piket-item-actions">
|
||||
<button class="btn btn-sm btn-outline" style="padding:3px 7px;font-size:10px"
|
||||
onclick="openModalUpdate(<?= $item['id'] ?>,'<?= $item['status'] ?>')">✏️</button>
|
||||
<form method="POST" style="display:inline" onsubmit="return confirm('Hapus jadwal ini?')">
|
||||
<input type="hidden" name="aksi" value="hapus">
|
||||
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" style="padding:3px 7px;font-size:10px">🗑</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
<?php $curDay->modify('+1 day'); endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Tambah Jadwal -->
|
||||
<div class="modal-bg" id="modalTambah">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">📅 Tambah Jadwal Piket</span>
|
||||
<div class="modal-close" onclick="closeModalTambah()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Tab: Single vs Bulk -->
|
||||
<div class="tabs-modal">
|
||||
<button class="tab-modal-btn active" id="tabSingleBtn" onclick="switchModalTab('single')">Satu Hari</button>
|
||||
<button class="tab-modal-btn" id="tabBulkBtn" onclick="switchModalTab('bulk')">Sekaligus (Range)</button>
|
||||
</div>
|
||||
|
||||
<!-- Form: Single -->
|
||||
<form method="POST" id="formSingle">
|
||||
<input type="hidden" name="aksi" value="tambah">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Pengurus <span style="color:var(--red)">*</span></label>
|
||||
<select name="user_id" required>
|
||||
<option value="">-- Pilih Pengurus --</option>
|
||||
<?php foreach ($pengurusList as $pg): ?>
|
||||
<option value="<?=$pg['id']?>"><?= htmlspecialchars($pg['nama']) ?> — <?= htmlspecialchars($pg['ibadah']??'-') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if ($role === 'admin'): ?>
|
||||
<div class="form-full form-group">
|
||||
<label>Rumah Ibadah <span style="color:var(--red)">*</span></label>
|
||||
<select name="rumah_ibadah_id" required>
|
||||
<option value="">-- Pilih --</option>
|
||||
<?php foreach ($rumahIbadahList as $ri): ?>
|
||||
<option value="<?=$ri['id']?>"><?= htmlspecialchars($ri['nama']) ?> (<?= $ri['jenis'] ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="rumah_ibadah_id" value="<?= $myRumahIbadahId ?>">
|
||||
<?php endif; ?>
|
||||
<div class="form-group">
|
||||
<label>Tanggal <span style="color:var(--red)">*</span></label>
|
||||
<input type="date" name="tanggal" required value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Shift <span style="color:var(--red)">*</span></label>
|
||||
<select name="shift" required>
|
||||
<?php foreach ($shiftLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jam Mulai</label>
|
||||
<input type="time" name="jam_mulai" value="08:00">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jam Selesai</label>
|
||||
<input type="time" name="jam_selesai" value="17:00">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Keterangan</label>
|
||||
<input type="text" name="keterangan" placeholder="Opsional — misal: tugas khusus verifikasi laporan">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalTambah()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">💾 Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Form: Bulk -->
|
||||
<form method="POST" id="formBulk" style="display:none">
|
||||
<input type="hidden" name="aksi" value="bulk">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Pengurus <span style="color:var(--red)">*</span></label>
|
||||
<select name="user_id" required>
|
||||
<option value="">-- Pilih Pengurus --</option>
|
||||
<?php foreach ($pengurusList as $pg): ?>
|
||||
<option value="<?=$pg['id']?>"><?= htmlspecialchars($pg['nama']) ?> — <?= htmlspecialchars($pg['ibadah']??'-') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php if ($role === 'admin'): ?>
|
||||
<div class="form-full form-group">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select name="rumah_ibadah_id" required>
|
||||
<option value="">-- Pilih --</option>
|
||||
<?php foreach ($rumahIbadahList as $ri): ?>
|
||||
<option value="<?=$ri['id']?>"><?= htmlspecialchars($ri['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="rumah_ibadah_id" value="<?= $myRumahIbadahId ?>">
|
||||
<?php endif; ?>
|
||||
<div class="form-group">
|
||||
<label>Dari Tanggal</label>
|
||||
<input type="date" name="tanggal_mulai" value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sampai Tanggal</label>
|
||||
<input type="date" name="tanggal_selesai" value="<?= date('Y-m-d', strtotime('+1 month')) ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Shift</label>
|
||||
<select name="shift">
|
||||
<?php foreach ($shiftLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jam Mulai</label>
|
||||
<input type="time" name="jam_mulai" value="08:00">
|
||||
</div>
|
||||
<div class="form-full form-group" style="grid-column:2">
|
||||
<label>Jam Selesai</label>
|
||||
<input type="time" name="jam_selesai" value="17:00">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Aktif Hari <span style="color:var(--red)">*</span></label>
|
||||
<div class="hari-grid">
|
||||
<?php
|
||||
$hariNama = ['0'=>'Min','1'=>'Sen','2'=>'Sel','3'=>'Rab','4'=>'Kam','5'=>'Jum','6'=>'Sab'];
|
||||
foreach ($hariNama as $v=>$l): ?>
|
||||
<div>
|
||||
<input type="checkbox" name="hari_aktif[]" id="h<?=$v?>" value="<?=$v?>" class="hari-opt"
|
||||
<?= in_array($v,['1','2','3','4','5'])?'checked':'' ?>>
|
||||
<label for="h<?=$v?>" class="hari-label"><?=$l?></label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalTambah()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">📅 Buat Jadwal Sekaligus</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Update Status -->
|
||||
<div class="modal-bg" id="modalUpdate">
|
||||
<div class="modal" style="max-width:400px">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">✏️ Update Status Piket</span>
|
||||
<div class="modal-close" onclick="closeModalUpdate()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="aksi" value="update_status">
|
||||
<input type="hidden" name="id" id="updateId">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Status</label>
|
||||
<select name="status" id="updateStatus">
|
||||
<?php foreach ($statusLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-full form-group" id="gantiWrap" style="display:none">
|
||||
<label>Pengurus Pengganti</label>
|
||||
<select name="digantikan_oleh">
|
||||
<option value="">-- Pilih --</option>
|
||||
<?php foreach ($pengurusList as $pg): ?>
|
||||
<option value="<?=$pg['id']?>"><?= htmlspecialchars($pg['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalUpdate()">Batal</button>
|
||||
<button type="submit" class="btn btn-success">💾 Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModalTambah(){ document.getElementById('modalTambah').classList.add('open'); }
|
||||
function closeModalTambah(){ document.getElementById('modalTambah').classList.remove('open'); }
|
||||
function openModalUpdate(id, status) {
|
||||
document.getElementById('updateId').value = id;
|
||||
document.getElementById('updateStatus').value = status;
|
||||
toggleGantiField(status);
|
||||
document.getElementById('modalUpdate').classList.add('open');
|
||||
}
|
||||
function closeModalUpdate(){ document.getElementById('modalUpdate').classList.remove('open'); }
|
||||
document.querySelectorAll('.modal-bg').forEach(m => m.addEventListener('click', function(e){ if(e.target===this) this.classList.remove('open'); }));
|
||||
|
||||
function switchModalTab(tab) {
|
||||
document.getElementById('formSingle').style.display = tab==='single'?'block':'none';
|
||||
document.getElementById('formBulk').style.display = tab==='bulk'?'block':'none';
|
||||
document.getElementById('tabSingleBtn').classList.toggle('active', tab==='single');
|
||||
document.getElementById('tabBulkBtn').classList.toggle('active', tab==='bulk');
|
||||
}
|
||||
|
||||
function toggleGantiField(val) {
|
||||
document.getElementById('gantiWrap').style.display = val==='digantikan'?'flex':'none';
|
||||
}
|
||||
document.getElementById('updateStatus').addEventListener('change', function(){ toggleGantiField(this.value); });
|
||||
|
||||
<?php if ($msg): ?>
|
||||
(function(){ var t=document.createElement('div'); t.className='toast success'; t.textContent='✅ <?= addslashes($msg) ?>'; document.getElementById('toastCont').appendChild(t); setTimeout(()=>t.remove(),3500); })();
|
||||
<?php endif; if ($err): ?>
|
||||
(function(){ var t=document.createElement('div'); t.className='toast error'; t.textContent='⚠️ <?= addslashes($err) ?>'; document.getElementById('toastCont').appendChild(t); setTimeout(()=>t.remove(),4000); })();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
require_once 'koneksi.php';
|
||||
requireLogin();
|
||||
|
||||
$msg = '';
|
||||
$err = '';
|
||||
$role = getRole();
|
||||
|
||||
// Load rumah ibadah untuk dropdown
|
||||
$ibadahList = $conn->query("SELECT id, nama, jenis FROM rumah_ibadah WHERE aktif=1 ORDER BY nama")->fetch_all(MYSQLI_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Manajemen Keluarga (KK) — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;
|
||||
--border:#243044;--border2:#2f3f57;
|
||||
--text:#e8edf3;--muted:#6b7e96;
|
||||
--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--purple:#9b59b6;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;
|
||||
}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s,color .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(232,80,80,.12);color:var(--red);}
|
||||
.container{max-width:1300px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:22px;}
|
||||
|
||||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap;}
|
||||
.search-box{flex:1;min-width:200px;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:9px 14px;color:var(--text);font-family:var(--font-body);font-size:13px;outline:none;}
|
||||
.search-box:focus{border-color:var(--red);}
|
||||
.btn{padding:9px 18px;border:none;border-radius:10px;font-family:var(--font-body);font-size:13px;font-weight:600;cursor:pointer;transition:opacity .2s;}
|
||||
.btn:hover{opacity:.88;}
|
||||
.btn-primary{background:var(--red);color:#fff;box-shadow:0 3px 12px rgba(232,80,80,.3);}
|
||||
.btn-success{background:var(--green);color:#fff;}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
|
||||
.btn-sm{padding:5px 12px;font-size:12px;border-radius:8px;}
|
||||
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;margin-bottom:28px;}
|
||||
.tbl-wrap{overflow-x:auto;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
th{text-align:left;padding:10px 14px;background:var(--surface2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border);}
|
||||
td{padding:10px 14px;border-bottom:1px solid var(--border2);vertical-align:middle;}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;text-transform:uppercase;}
|
||||
.b-miskin{background:rgba(232,80,80,.2);color:var(--red);}
|
||||
.b-rentan{background:rgba(240,180,41,.2);color:var(--yellow);}
|
||||
.b-mampu{background:rgba(39,174,114,.2);color:var(--green);}
|
||||
.b-belum{background:rgba(107,126,150,.15);color:var(--muted);}
|
||||
.b-terv{background:rgba(39,174,114,.2);color:var(--green);}
|
||||
.act{display:flex;gap:5px;}
|
||||
|
||||
/* MODAL */
|
||||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:500;align-items:center;justify-content:center;padding:20px;}
|
||||
.modal-bg.open{display:flex;}
|
||||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:20px;width:100%;max-width:640px;max-height:90vh;overflow-y:auto;box-shadow:0 24px 80px rgba(0,0,0,.5);}
|
||||
.modal-header{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);z-index:1;}
|
||||
.modal-title{font-family:var(--font-head);font-size:16px;font-weight:700;flex:1;}
|
||||
.modal-close{width:30px;height:30px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);font-size:16px;}
|
||||
.modal-body{padding:22px;}
|
||||
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
.form-full{grid-column:1/-1;}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;}
|
||||
.form-group label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:500;}
|
||||
.form-group input,.form-group select,.form-group textarea{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 13px;color:var(--text);font-family:var(--font-body);font-size:13.5px;outline:none;transition:border-color .2s;}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--red);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
.form-section{font-family:var(--font-head);font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;padding-bottom:8px;border-bottom:1px solid var(--border);grid-column:1/-1;margin-top:6px;}
|
||||
.form-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px;}
|
||||
|
||||
/* DETAIL SIDE PANEL */
|
||||
.side-panel{position:fixed;right:0;top:0;bottom:0;width:400px;background:var(--surface);border-left:1px solid var(--border);z-index:400;transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;}
|
||||
.side-panel.open{transform:translateX(0);}
|
||||
.sp-header{padding:18px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
|
||||
.sp-title{font-family:var(--font-head);font-size:15px;font-weight:700;flex:1;}
|
||||
.sp-body{flex:1;overflow-y:auto;padding:18px 20px;}
|
||||
.sp-section{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;margin:14px 0 8px;font-weight:600;}
|
||||
.sp-row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border2);font-size:13px;}
|
||||
.sp-row span:first-child{color:var(--muted);}
|
||||
.anggota-card{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px;}
|
||||
.anggota-name{font-weight:600;font-size:13px;margin-bottom:4px;}
|
||||
.anggota-meta{font-size:11px;color:var(--muted);}
|
||||
|
||||
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
||||
.toast{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 16px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideIn .2s ease;min-width:260px;}
|
||||
.toast.success{border-left:3px solid var(--green);}
|
||||
.toast.error{border-left:3px solid var(--red);}
|
||||
@keyframes slideIn{from{transform:translateX(20px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
|
||||
.empty{text-align:center;color:var(--muted);padding:40px;font-size:14px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="keluarga.php" class="nav-link active">Keluarga</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted)">👤 <?= htmlspecialchars($_SESSION['nama']) ?></div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>🏠 Manajemen Data Keluarga (Kartu Keluarga)</h2>
|
||||
|
||||
<div class="toolbar">
|
||||
<input type="text" class="search-box" placeholder="🔍 Cari No. KK / Alamat..." oninput="loadTable(this.value)">
|
||||
<select id="filterVer" onchange="loadTable()" style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:9px 12px;color:var(--text);font-size:13px;outline:none;">
|
||||
<option value="">Semua Verifikasi</option>
|
||||
<option value="belum">Belum Terverifikasi</option>
|
||||
<option value="terverifikasi">Terverifikasi</option>
|
||||
</select>
|
||||
<?php if(hasRole('admin','pengurus')): ?>
|
||||
<button class="btn btn-primary" onclick="openModal()">+ Tambah KK</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No. KK</th>
|
||||
<th>Alamat</th>
|
||||
<th>RT/RW</th>
|
||||
<th>Kecamatan</th>
|
||||
<th>Anggota</th>
|
||||
<th>Miskin</th>
|
||||
<th>Status Ekonomi</th>
|
||||
<th>Verifikasi</th>
|
||||
<th>Rumah Ibadah</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr><td colspan="10" class="empty">Memuat data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL FORM -->
|
||||
<div class="modal-bg" id="formModal" onclick="if(event.target.id==='formModal')closeModal()">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modalTitle">Tambah Kartu Keluarga</div>
|
||||
<div class="modal-close" onclick="closeModal()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formKK" onsubmit="submitKK(event)">
|
||||
<input type="hidden" id="fk_id" name="id">
|
||||
<div class="form-grid">
|
||||
<div class="form-section">📋 Identitas Keluarga</div>
|
||||
<div class="form-group form-full">
|
||||
<label>No. KK (16 digit) *</label>
|
||||
<input type="text" name="no_kk" id="fk_nokk" maxlength="16" minlength="16" pattern="\d{16}" placeholder="6171xxxxxxxxxxxx" required>
|
||||
</div>
|
||||
<div class="form-group form-full">
|
||||
<label>Alamat Lengkap *</label>
|
||||
<input type="text" name="alamat" id="fk_alamat" required placeholder="Jl. Contoh No. 1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>RT</label>
|
||||
<input type="text" name="rt" id="fk_rt" maxlength="5" placeholder="001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>RW</label>
|
||||
<input type="text" name="rw" id="fk_rw" maxlength="5" placeholder="001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kelurahan</label>
|
||||
<input type="text" name="kelurahan" id="fk_kel" placeholder="Nama kelurahan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kecamatan</label>
|
||||
<input type="text" name="kecamatan" id="fk_kec" placeholder="Nama kecamatan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kota</label>
|
||||
<input type="text" name="kota" id="fk_kota" value="Pontianak">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status Ekonomi *</label>
|
||||
<select name="status_ekonomi" id="fk_status" required>
|
||||
<option value="miskin">Miskin</option>
|
||||
<option value="rentan" selected>Rentan</option>
|
||||
<option value="mampu">Mampu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-section">🕌 Rumah Ibadah & Lokasi</div>
|
||||
<div class="form-group form-full">
|
||||
<label>Rumah Ibadah Terdekat</label>
|
||||
<select name="rumah_ibadah_id" id="fk_ibadah">
|
||||
<option value="">-- Pilih (opsional, auto-detect dari koordinat) --</option>
|
||||
<?php foreach($ibadahList as $ri): ?>
|
||||
<option value="<?= $ri['id'] ?>">[<?= $ri['jenis'] ?>] <?= htmlspecialchars($ri['nama']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Latitude</label>
|
||||
<input type="number" name="lat" id="fk_lat" step="0.000001" placeholder="-0.026300">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Longitude</label>
|
||||
<input type="number" name="lng" id="fk_lng" step="0.000001" placeholder="109.342500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModal()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">💾 Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SIDE PANEL DETAIL -->
|
||||
<div class="side-panel" id="sidePanel">
|
||||
<div class="sp-header">
|
||||
<div class="sp-title" id="spTitle">Detail Keluarga</div>
|
||||
<div class="modal-close" onclick="closeSide()">✕</div>
|
||||
</div>
|
||||
<div class="sp-body" id="spBody">
|
||||
<div class="empty">Pilih keluarga untuk melihat detail</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load tabel
|
||||
function loadTable(search = '') {
|
||||
const ver = document.getElementById('filterVer')?.value || '';
|
||||
fetch(`api/keluarga.php?action=list&search=${encodeURIComponent(search)}&verifikasi=${ver}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
if (!data.success || !data.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="empty">Tidak ada data keluarga</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = data.data.map(k => `
|
||||
<tr>
|
||||
<td style="font-family:monospace;font-size:11px">${k.no_kk}</td>
|
||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${k.alamat}">${k.alamat}</td>
|
||||
<td style="font-size:11px">${k.rt||'-'}/${k.rw||'-'}</td>
|
||||
<td style="font-size:12px">${k.kecamatan||'-'}</td>
|
||||
<td style="text-align:center">${k.jumlah_anggota||0} org</td>
|
||||
<td style="text-align:center">${k.jumlah_miskin > 0 ? `<span class="badge b-miskin">${k.jumlah_miskin}</span>` : '<span style="color:var(--muted)">-</span>'}</td>
|
||||
<td><span class="badge b-${k.status_ekonomi}">${k.status_ekonomi}</span></td>
|
||||
<td><span class="badge ${k.status_verifikasi==='terverifikasi'?'b-terv':'b-belum'}">${k.status_verifikasi}</span></td>
|
||||
<td style="font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${k.nama_ibadah||'<span style="color:var(--muted)">-</span>'}</td>
|
||||
<td>
|
||||
<div class="act">
|
||||
<button class="btn btn-outline btn-sm" onclick="lihatDetail(${k.id})" title="Detail">👁</button>
|
||||
<?php if(hasRole('admin','pengurus')): ?>
|
||||
<button class="btn btn-outline btn-sm" onclick="editKK(${k.id})" title="Edit">✏️</button>
|
||||
${k.status_verifikasi !== 'terverifikasi' ? `<button class="btn btn-success btn-sm" onclick="verifikasi(${k.id})" title="Verifikasi">✓</button>` : ''}
|
||||
<button class="btn btn-outline btn-sm" onclick="hapusKK(${k.id},'${k.no_kk}')" title="Hapus" style="color:var(--red)">🗑</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
// Modal
|
||||
function openModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Tambah Kartu Keluarga';
|
||||
document.getElementById('formKK').reset();
|
||||
document.getElementById('fk_id').value = '';
|
||||
document.getElementById('formModal').classList.add('open');
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('formModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function editKK(id) {
|
||||
fetch(`api/keluarga.php?action=get&id=${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
const k = data.data;
|
||||
document.getElementById('modalTitle').textContent = 'Edit KK: ' + k.no_kk;
|
||||
document.getElementById('fk_id').value = k.id;
|
||||
document.getElementById('fk_nokk').value = k.no_kk;
|
||||
document.getElementById('fk_alamat').value = k.alamat;
|
||||
document.getElementById('fk_rt').value = k.rt || '';
|
||||
document.getElementById('fk_rw').value = k.rw || '';
|
||||
document.getElementById('fk_kel').value = k.kelurahan || '';
|
||||
document.getElementById('fk_kec').value = k.kecamatan || '';
|
||||
document.getElementById('fk_kota').value = k.kota || 'Pontianak';
|
||||
document.getElementById('fk_status').value = k.status_ekonomi;
|
||||
document.getElementById('fk_ibadah').value = k.rumah_ibadah_id || '';
|
||||
document.getElementById('fk_lat').value = k.lat || '';
|
||||
document.getElementById('fk_lng').value = k.lng || '';
|
||||
document.getElementById('formModal').classList.add('open');
|
||||
});
|
||||
}
|
||||
|
||||
function submitKK(e) {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
fetch('api/keluarga.php?action=save', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) { toast('✅ ' + data.message, 'success'); closeModal(); loadTable(); }
|
||||
else toast('❌ ' + data.error, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function verifikasi(id) {
|
||||
if (!confirm('Verifikasi keluarga ini?')) return;
|
||||
fetch('api/keluarga.php?action=verifikasi', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `id=${id}`
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) { toast('✅ Keluarga terverifikasi', 'success'); loadTable(); }
|
||||
else toast('❌ ' + data.error, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function hapusKK(id, nokk) {
|
||||
if (!confirm(`Hapus KK ${nokk}? Pastikan tidak ada anggota aktif.`)) return;
|
||||
fetch('api/keluarga.php?action=delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `id=${id}`
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) { toast('🗑 KK dihapus', 'success'); loadTable(); }
|
||||
else toast('❌ ' + data.error, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// DETAIL SIDE PANEL
|
||||
function lihatDetail(id) {
|
||||
fetch(`api/keluarga.php?action=get&id=${id}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
const k = data.data;
|
||||
document.getElementById('spTitle').textContent = 'KK: ' + k.no_kk;
|
||||
const anggotaHtml = (k.anggota || []).map(a => `
|
||||
<div class="anggota-card">
|
||||
<div class="anggota-name">${a.nama} <span class="badge b-${a.status_ekonomi}" style="font-size:9px">${a.status_ekonomi}</span></div>
|
||||
<div class="anggota-meta">${a.status_keluarga.replace('_',' ')} · ${a.umur} thn · ${a.pekerjaan||'Tidak bekerja'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('spBody').innerHTML = `
|
||||
<div class="sp-section">Identitas</div>
|
||||
<div class="sp-row"><span>No. KK</span><span style="font-family:monospace;font-size:11px">${k.no_kk}</span></div>
|
||||
<div class="sp-row"><span>Alamat</span><span>${k.alamat}</span></div>
|
||||
<div class="sp-row"><span>RT/RW</span><span>${k.rt||'-'}/${k.rw||'-'}</span></div>
|
||||
<div class="sp-row"><span>Kelurahan</span><span>${k.kelurahan||'-'}</span></div>
|
||||
<div class="sp-row"><span>Kecamatan</span><span>${k.kecamatan||'-'}</span></div>
|
||||
<div class="sp-row"><span>Status Ekonomi</span><span><span class="badge b-${k.status_ekonomi}">${k.status_ekonomi}</span></span></div>
|
||||
<div class="sp-row"><span>Verifikasi</span><span>${k.status_verifikasi}</span></div>
|
||||
<div class="sp-row"><span>Rumah Ibadah</span><span>${k.nama_ibadah||'-'}</span></div>
|
||||
<div class="sp-row"><span>Koordinat</span><span style="font-size:11px">${k.lat||'?'}, ${k.lng||'?'}</span></div>
|
||||
<div class="sp-section">Anggota Keluarga (${(k.anggota||[]).length} orang)</div>
|
||||
${anggotaHtml || '<div style="color:var(--muted);font-size:13px;padding:10px 0">Belum ada anggota terdaftar</div>'}
|
||||
<div style="margin-top:14px">
|
||||
<a href="index.php" style="color:var(--blue);font-size:13px;text-decoration:none">→ Lihat di Peta</a>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('sidePanel').classList.add('open');
|
||||
});
|
||||
}
|
||||
|
||||
function closeSide() {
|
||||
document.getElementById('sidePanel').classList.remove('open');
|
||||
}
|
||||
|
||||
function toast(msg, type = 'info') {
|
||||
const c = document.getElementById('toastContainer');
|
||||
const t = document.createElement('div');
|
||||
t.className = `toast ${type}`;
|
||||
t.textContent = msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
|
||||
loadTable();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// koneksi.php — Database connection, session, helpers
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST'));
|
||||
define('DB_USER', getenv('DB_USER'));
|
||||
define('DB_PASS', getenv('DB_PASSWORD'));
|
||||
define('DB_NAME', getenv('DB_NAME'));
|
||||
define('APP_NAME', 'WebGIS Kemiskinan');
|
||||
define('APP_VERSION', '2.0');
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
define('UPLOAD_URL', 'uploads/');
|
||||
|
||||
// -- Database
|
||||
$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, 'error' => 'Koneksi database gagal: ' . $conn->connect_error]));
|
||||
}
|
||||
|
||||
// -- Session
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AUTH HELPERS
|
||||
// ============================================================
|
||||
function isLoggedIn(): bool {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!isLoggedIn()) {
|
||||
if (isApiRequest()) {
|
||||
http_response_code(401);
|
||||
die(json_encode(['success' => false, 'error' => 'Unauthorized']));
|
||||
}
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function getRole(): string {
|
||||
return $_SESSION['role_slug'] ?? 'masyarakat';
|
||||
}
|
||||
|
||||
function hasRole(string ...$roles): bool {
|
||||
return in_array(getRole(), $roles);
|
||||
}
|
||||
|
||||
function requireRole(string ...$roles): void {
|
||||
requireLogin();
|
||||
if (!hasRole(...$roles)) {
|
||||
if (isApiRequest()) {
|
||||
http_response_code(403);
|
||||
die(json_encode(['success' => false, 'error' => 'Akses ditolak']));
|
||||
}
|
||||
header('Location: index.php?err=forbidden');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function isApiRequest(): bool {
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) ||
|
||||
(isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json'));
|
||||
}
|
||||
|
||||
function currentUserId(): int {
|
||||
return (int)($_SESSION['user_id'] ?? 0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DB HELPERS
|
||||
// ============================================================
|
||||
function sanitize($conn, $val): string {
|
||||
return $conn->real_escape_string(trim((string)$val));
|
||||
}
|
||||
|
||||
function dbQuery($conn, string $sql, array $params = [], string $types = ''): ?array {
|
||||
if (empty($params)) {
|
||||
$res = $conn->query($sql);
|
||||
if (!$res) return null;
|
||||
return $res->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) return null;
|
||||
if ($types && $params) {
|
||||
$stmt->bind_param($types, ...$params);
|
||||
}
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
|
||||
}
|
||||
|
||||
function dbFirst($conn, string $sql, array $params = [], string $types = ''): ?array {
|
||||
$rows = dbQuery($conn, $sql, $params, $types);
|
||||
return $rows[0] ?? null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RESPONSE HELPERS
|
||||
// ============================================================
|
||||
function jsonSuccess($data = null, string $msg = 'OK'): void {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'data' => $data]);
|
||||
exit;
|
||||
}
|
||||
|
||||
function jsonError(string $msg, int $code = 400): void {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code($code);
|
||||
echo json_encode(['success' => false, 'error' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// VALIDASI PENDIDIKAN vs UMUR
|
||||
// ============================================================
|
||||
function validasiPendidikan(int $umur, string $status_pendidikan, string $pendidikan_terakhir): array {
|
||||
$warnings = [];
|
||||
|
||||
if ($umur >= 7 && $umur <= 12) {
|
||||
if ($status_pendidikan === 'tidak_sekolah') {
|
||||
$warnings[] = "⚠️ Usia $umur tahun seharusnya masih SD. Status tidak sekolah perlu verifikasi.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($umur >= 13 && $umur <= 15) {
|
||||
if ($status_pendidikan === 'tidak_sekolah' && !in_array($pendidikan_terakhir, ['sd','smp'])) {
|
||||
$warnings[] = "⚠️ Usia $umur tahun seharusnya SMP. Mohon verifikasi status pendidikan.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($umur >= 16 && $umur <= 18) {
|
||||
if ($status_pendidikan === 'tidak_sekolah' && !in_array($pendidikan_terakhir, ['sd','smp','sma','smk'])) {
|
||||
$warnings[] = "⚠️ Usia $umur tahun seharusnya SMA/SMK.";
|
||||
}
|
||||
}
|
||||
|
||||
if ($umur < 7 && $status_pendidikan === 'masih_sekolah' && in_array($pendidikan_terakhir, ['sma','smk','s1','s2','s3'])) {
|
||||
$warnings[] = "❌ Data tidak valid: usia $umur tahun tidak mungkin sudah $pendidikan_terakhir.";
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GEOGRAPHIC HELPERS
|
||||
// ============================================================
|
||||
function hitungJarak(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371000;
|
||||
$phi1 = deg2rad($lat1); $phi2 = deg2rad($lat2);
|
||||
$dphi = deg2rad($lat2 - $lat1);
|
||||
$dlam = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dphi/2)**2 + cos($phi1)*cos($phi2)*sin($dlam/2)**2;
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
}
|
||||
|
||||
function cariRumahIbadahTerdekat($conn, float $lat, float $lng): ?array {
|
||||
$rows = $conn->query("SELECT id, lat, lng, radius FROM rumah_ibadah WHERE aktif=1")->fetch_all(MYSQLI_ASSOC);
|
||||
$terdekat = null; $minJarak = PHP_INT_MAX;
|
||||
foreach ($rows as $ri) {
|
||||
$d = hitungJarak($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
|
||||
if ($d < $minJarak) {
|
||||
$minJarak = $d;
|
||||
$terdekat = $ri;
|
||||
$terdekat['jarak'] = $d;
|
||||
}
|
||||
}
|
||||
return $terdekat;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LOG AKTIVITAS
|
||||
// ============================================================
|
||||
function logAktivitas($conn, string $aksi, string $tabel = '', int $id = 0, $dataLama = null, $dataBaru = null): void {
|
||||
$userId = currentUserId() ?: 'NULL';
|
||||
$ip = sanitize($conn, $_SERVER['REMOTE_ADDR'] ?? '');
|
||||
$ua = sanitize($conn, substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255));
|
||||
$aksiSan = sanitize($conn, $aksi);
|
||||
$tabelSan = sanitize($conn, $tabel);
|
||||
$lamaJson = $dataLama ? "'" . sanitize($conn, json_encode($dataLama, JSON_UNESCAPED_UNICODE)) . "'" : 'NULL';
|
||||
$baruJson = $dataBaru ? "'" . sanitize($conn, json_encode($dataBaru, JSON_UNESCAPED_UNICODE)) . "'" : 'NULL';
|
||||
$conn->query("INSERT INTO log_aktivitas (user_id, aksi, tabel_target, id_target, data_lama, data_baru, ip_address, user_agent)
|
||||
VALUES ($userId, '$aksiSan', '$tabelSan', $id, $lamaJson, $baruJson, '$ip', '$ua')");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UPLOAD FILE
|
||||
// ============================================================
|
||||
function uploadFile(array $file, string $prefix = 'file', array $allowed = ['jpg','jpeg','png','pdf']): ?string {
|
||||
if (empty($file['name'])) return null;
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowed)) return null;
|
||||
if ($file['size'] > 5 * 1024 * 1024) return null; // 5MB max
|
||||
$fname = $prefix . '_' . time() . '_' . rand(100, 999) . '.' . $ext;
|
||||
if (move_uploaded_file($file['tmp_name'], UPLOAD_DIR . $fname)) return $fname;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NOTIFIKASI
|
||||
// ============================================================
|
||||
function kirimNotifikasi($conn, int $userId, string $judul, string $isi, string $tipe = 'info', string $url = ''): void {
|
||||
$judulSan = sanitize($conn, $judul);
|
||||
$isiSan = sanitize($conn, $isi);
|
||||
$tipeSan = sanitize($conn, $tipe);
|
||||
$urlSan = sanitize($conn, $url);
|
||||
$conn->query("INSERT INTO notifikasi (user_id, judul, isi, tipe, url_target) VALUES ($userId, '$judulSan', '$isiSan', '$tipeSan', '$urlSan')");
|
||||
}
|
||||
|
||||
function jumlahNotifBelumDibaca($conn): int {
|
||||
$uid = currentUserId();
|
||||
if (!$uid) return 0;
|
||||
$r = $conn->query("SELECT COUNT(*) as n FROM notifikasi WHERE user_id=$uid AND dibaca=0");
|
||||
return (int)($r->fetch_assoc()['n'] ?? 0);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// lapor_publik.php — Form laporan TANPA LOGIN
|
||||
// Akses publik: warga bisa lapor via QR code di masjid
|
||||
// ============================================================
|
||||
require_once 'koneksi.php';
|
||||
|
||||
$msg = ''; $err = ''; $kode = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$nama = trim($_POST['nama_pelapor'] ?? '');
|
||||
$no_hp = trim($_POST['no_hp_pelapor'] ?? '');
|
||||
$nama_lapor = trim($_POST['nama_terlapor'] ?? '');
|
||||
$alamat = trim($_POST['alamat_kejadian'] ?? '');
|
||||
$deskripsi = trim($_POST['deskripsi'] ?? '');
|
||||
$kategori = $_POST['kategori'] ?? 'kemiskinan';
|
||||
$urgensi = $_POST['urgensi'] ?? 'sedang';
|
||||
$lat = floatval($_POST['lat'] ?? 0);
|
||||
$lng = floatval($_POST['lng'] ?? 0);
|
||||
|
||||
if (strlen($deskripsi) < 20) {
|
||||
$err = 'Deskripsi terlalu singkat. Mohon jelaskan kondisi lebih detail (min. 20 karakter).';
|
||||
} else {
|
||||
// Generate kode laporan unik
|
||||
$kode = 'LPR-' . date('Y') . '-' . strtoupper(substr(uniqid(), -6));
|
||||
|
||||
// Upload foto jika ada
|
||||
$foto = null;
|
||||
if (!empty($_FILES['foto']['name'])) {
|
||||
$foto = uploadFile($_FILES['foto'], 'laporan', ['jpg','jpeg','png']);
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO laporan
|
||||
(kode_laporan, nama_pelapor, no_hp_pelapor, nama_terlapor, alamat_kejadian,
|
||||
lat, lng, deskripsi, kategori, urgensi, foto, status)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,'pending')");
|
||||
$namaVal = $nama ?: null;
|
||||
$hpVal = $no_hp ?: null;
|
||||
$laporVal = $nama_lapor ?: null;
|
||||
$latVal = $lat ?: null;
|
||||
$lngVal = $lng ?: null;
|
||||
$stmt->bind_param('sssssddssss',
|
||||
$kode, $namaVal, $hpVal, $laporVal, $alamat,
|
||||
$latVal, $lngVal, $deskripsi, $kategori, $urgensi, $foto);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
logAktivitas($conn, 'LAPORAN_PUBLIK', 'laporan', $conn->insert_id);
|
||||
$msg = $kode;
|
||||
} else {
|
||||
$err = 'Gagal menyimpan laporan. Silakan coba lagi.';
|
||||
$kode = '';
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Lapor Warga — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;
|
||||
--border:#243044;--border2:#2f3f57;
|
||||
--text:#e8edf3;--muted:#6b7e96;
|
||||
--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;
|
||||
}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:24px 16px;}
|
||||
.header{text-align:center;margin-bottom:28px;}
|
||||
.header h1{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:6px;}
|
||||
.header p{font-size:13px;color:var(--muted);}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:20px;width:100%;max-width:540px;padding:28px;}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;margin-bottom:16px;}
|
||||
.form-group label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:600;}
|
||||
.form-group input,.form-group select,.form-group textarea{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:11px 14px;color:var(--text);font-family:var(--font-body);font-size:14px;outline:none;transition:border-color .2s;width:100%;}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--red);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
.form-group textarea{resize:vertical;min-height:100px;}
|
||||
.form-section{font-family:var(--font-head);font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;padding:10px 0 8px;border-bottom:1px solid var(--border);margin-bottom:16px;}
|
||||
.hint{font-size:11px;color:var(--muted);margin-top:2px;}
|
||||
.badge-anonim{background:rgba(107,126,150,.15);color:var(--muted);font-size:11px;padding:3px 10px;border-radius:20px;display:inline-block;margin-bottom:6px;}
|
||||
.urgensi-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;}
|
||||
.urgensi-opt{display:none;}
|
||||
.urgensi-label{display:flex;flex-direction:column;align-items:center;gap:5px;padding:12px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-size:12px;text-align:center;transition:all .15s;}
|
||||
.urgensi-opt:checked + .urgensi-label{border-color:var(--red);background:rgba(232,80,80,.1);color:var(--red);}
|
||||
.urgensi-label .icon{font-size:22px;}
|
||||
.btn{width:100%;padding:13px;background:var(--red);color:#fff;border:none;border-radius:12px;font-family:var(--font-body);font-size:15px;font-weight:600;cursor:pointer;box-shadow:0 4px 16px rgba(232,80,80,.3);transition:opacity .2s;}
|
||||
.btn:hover{opacity:.88;}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed;}
|
||||
.alert{padding:14px 16px;border-radius:12px;font-size:13px;margin-bottom:20px;}
|
||||
.alert-err{background:rgba(232,80,80,.15);border:1px solid rgba(232,80,80,.3);color:var(--red);}
|
||||
.alert-ok{background:rgba(39,174,114,.12);border:1px solid rgba(39,174,114,.3);color:var(--green);text-align:center;}
|
||||
.kode-laporan{font-family:var(--font-head);font-size:26px;font-weight:800;letter-spacing:2px;margin:8px 0;}
|
||||
.loc-btn{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 14px;color:var(--muted);font-size:13px;cursor:pointer;width:100%;text-align:left;transition:border-color .2s;}
|
||||
.loc-btn:hover{border-color:var(--blue);}
|
||||
.loc-ok{color:var(--green);font-size:11px;margin-top:4px;}
|
||||
footer{margin-top:28px;font-size:11px;color:var(--muted);text-align:center;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h1>📢 Lapor Kondisi Warga</h1>
|
||||
<p>WebGIS Pengentasan Kemiskinan — Kota Pontianak</p>
|
||||
<p style="margin-top:6px;font-size:12px;">Laporan anonim diperbolehkan. Tidak perlu login.</p>
|
||||
</div>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="card" style="max-width:540px;text-align:center;">
|
||||
<div style="font-size:48px;margin-bottom:12px;">✅</div>
|
||||
<div class="alert alert-ok">
|
||||
<div style="margin-bottom:8px;">Laporan berhasil dikirim!</div>
|
||||
<div>Kode laporan kamu:</div>
|
||||
<div class="kode-laporan"><?= htmlspecialchars($msg) ?></div>
|
||||
<div style="font-size:12px;color:var(--muted);">Simpan kode ini untuk cek status laporan.</div>
|
||||
</div>
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
|
||||
Pengurus rumah ibadah terdekat sudah mendapat notifikasi dan akan segera menindaklanjuti.
|
||||
</p>
|
||||
<a href="lapor_publik.php" style="display:block;padding:12px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;color:var(--text);text-decoration:none;font-size:13px;">
|
||||
+ Buat Laporan Baru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="card">
|
||||
<?php if ($err): ?>
|
||||
<div class="alert alert-err">⚠️ <?= htmlspecialchars($err) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" id="formLapor">
|
||||
|
||||
<div class="form-section">Identitas Pelapor (opsional)</div>
|
||||
<span class="badge-anonim">🔒 Boleh anonim — tidak wajib diisi</span>
|
||||
<div class="form-group">
|
||||
<label>Nama Pelapor</label>
|
||||
<input type="text" name="nama_pelapor" placeholder="Anonim" value="<?= htmlspecialchars($_POST['nama_pelapor'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. HP / WhatsApp</label>
|
||||
<input type="tel" name="no_hp_pelapor" placeholder="Untuk konfirmasi tindak lanjut (opsional)" value="<?= htmlspecialchars($_POST['no_hp_pelapor'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-section">Data yang Dilaporkan</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Warga yang Perlu Dibantu</label>
|
||||
<input type="text" name="nama_terlapor" placeholder="Nama lengkap / panggilan" value="<?= htmlspecialchars($_POST['nama_terlapor'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat Kejadian <span style="color:var(--red)">*</span></label>
|
||||
<input type="text" name="alamat_kejadian" required placeholder="Gang, RT/RW, Jalan..." value="<?= htmlspecialchars($_POST['alamat_kejadian'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" class="loc-btn" onclick="ambilLokasi()">📍 Gunakan Lokasi GPS Saya (opsional)</button>
|
||||
<span class="loc-ok" id="locOk" style="display:none">✅ Lokasi berhasil diambil</span>
|
||||
<input type="hidden" name="lat" id="inp_lat">
|
||||
<input type="hidden" name="lng" id="inp_lng">
|
||||
</div>
|
||||
|
||||
<div class="form-section">Detail Laporan</div>
|
||||
<div class="form-group">
|
||||
<label>Kategori</label>
|
||||
<select name="kategori">
|
||||
<?php foreach(['kemiskinan'=>'Kemiskinan','kesehatan'=>'Kesehatan','pendidikan'=>'Pendidikan','bencana'=>'Bencana','kekerasan'=>'Kekerasan','lainnya'=>'Lainnya'] as $v=>$l): ?>
|
||||
<option value="<?=$v?>" <?= ($_POST['kategori']??'kemiskinan')===$v?'selected':'' ?>><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tingkat Urgensi <span style="color:var(--red)">*</span></label>
|
||||
<div class="urgensi-grid">
|
||||
<?php
|
||||
$urgensiOpt = [
|
||||
'rendah' => ['🟢','Rendah','Tidak mendesak'],
|
||||
'sedang' => ['🟡','Sedang','Perlu diperhatikan'],
|
||||
'tinggi' => ['🟠','Tinggi','Segera ditangani'],
|
||||
'darurat' => ['🔴','Darurat','Bahaya / kritis'],
|
||||
];
|
||||
$selUrg = $_POST['urgensi'] ?? 'sedang';
|
||||
foreach ($urgensiOpt as $val => [$icon, $label, $sub]): ?>
|
||||
<div>
|
||||
<input type="radio" name="urgensi" id="urg_<?=$val?>" value="<?=$val?>" class="urgensi-opt" <?=$selUrg===$val?'checked':''?>>
|
||||
<label for="urg_<?=$val?>" class="urgensi-label">
|
||||
<span class="icon"><?=$icon?></span>
|
||||
<strong><?=$label?></strong>
|
||||
<span style="font-size:10px;color:var(--muted)"><?=$sub?></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Deskripsi Kondisi <span style="color:var(--red)">*</span></label>
|
||||
<textarea name="deskripsi" required placeholder="Ceritakan kondisi warga tersebut secara detail. Misal: sudah berapa lama sakit, apakah punya penghasilan, kondisi tempat tinggal, dsb."><?= htmlspecialchars($_POST['deskripsi'] ?? '') ?></textarea>
|
||||
<span class="hint">Minimal 20 karakter. Semakin detail, semakin mudah pengurus menindaklanjuti.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Foto Pendukung (opsional)</label>
|
||||
<input type="file" name="foto" accept="image/*" style="padding:8px;">
|
||||
<span class="hint">Max 5MB. Format: JPG, PNG.</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="btnKirim">📤 Kirim Laporan</button>
|
||||
<p style="text-align:center;font-size:11px;color:var(--muted);margin-top:12px;">
|
||||
Data laporan hanya digunakan untuk keperluan sosial dan tidak disebarluaskan.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer>
|
||||
WebGIS Pengentasan Kemiskinan — Kota Pontianak<br>
|
||||
Didukung oleh Rumah Ibadah & Dinas Sosial
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function ambilLokasi() {
|
||||
if (!navigator.geolocation) { alert('Browser tidak mendukung GPS'); return; }
|
||||
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||
document.getElementById('inp_lat').value = pos.coords.latitude;
|
||||
document.getElementById('inp_lng').value = pos.coords.longitude;
|
||||
document.getElementById('locOk').style.display = 'block';
|
||||
}, function() {
|
||||
alert('Gagal mengambil lokasi. Pastikan izin lokasi diaktifkan.');
|
||||
});
|
||||
}
|
||||
document.getElementById('formLapor')?.addEventListener('submit', function() {
|
||||
document.getElementById('btnKirim').disabled = true;
|
||||
document.getElementById('btnKirim').textContent = '⏳ Mengirim...';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// laporan.php — Manajemen Laporan Masyarakat
|
||||
// Akses: admin, pengurus
|
||||
// ============================================================
|
||||
require_once 'koneksi.php';
|
||||
requireRole('admin','pengurus');
|
||||
|
||||
$role = getRole();
|
||||
$myUid = currentUserId();
|
||||
|
||||
// Filter
|
||||
$filterStatus = $_GET['status'] ?? '';
|
||||
$filterKategori = $_GET['kategori'] ?? '';
|
||||
$filterUrgensi = $_GET['urgensi'] ?? '';
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
|
||||
$where = [];
|
||||
if ($filterStatus) $where[] = "l.status = '" . sanitize($conn, $filterStatus) . "'";
|
||||
if ($filterKategori) $where[] = "l.kategori = '" . sanitize($conn, $filterKategori) . "'";
|
||||
if ($filterUrgensi) $where[] = "l.urgensi = '" . sanitize($conn, $filterUrgensi) . "'";
|
||||
if ($q) {
|
||||
$qSan = sanitize($conn, $q);
|
||||
$where[] = "(l.kode_laporan LIKE '%$qSan%' OR l.nama_pelapor LIKE '%$qSan%' OR l.deskripsi LIKE '%$qSan%' OR l.alamat_kejadian LIKE '%$qSan%')";
|
||||
}
|
||||
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$laporanList = $conn->query("
|
||||
SELECT l.*, p.nama AS nama_penduduk,
|
||||
u1.nama AS nama_verifikator, u2.nama AS nama_prosesor
|
||||
FROM laporan l
|
||||
LEFT JOIN penduduk p ON l.penduduk_id = p.id
|
||||
LEFT JOIN users u1 ON l.diverifikasi_oleh = u1.id
|
||||
LEFT JOIN users u2 ON l.diproses_oleh = u2.id
|
||||
$whereStr
|
||||
ORDER BY FIELD(l.urgensi,'darurat','tinggi','sedang','rendah'), l.created_at DESC
|
||||
LIMIT 200
|
||||
")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Stats ringkas
|
||||
$stats = [
|
||||
'pending' => $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='pending'")->fetch_assoc()['c'],
|
||||
'diverifikasi' => $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='diverifikasi'")->fetch_assoc()['c'],
|
||||
'diproses' => $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='diproses'")->fetch_assoc()['c'],
|
||||
'selesai' => $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='selesai'")->fetch_assoc()['c'],
|
||||
'ditolak' => $conn->query("SELECT COUNT(*) c FROM laporan WHERE status='ditolak'")->fetch_assoc()['c'],
|
||||
];
|
||||
|
||||
$statusLabel = [
|
||||
'pending' => ['label'=>'Pending', 'color'=>'#f0b429'],
|
||||
'diverifikasi' => ['label'=>'Diverifikasi', 'color'=>'#4a9eff'],
|
||||
'diproses' => ['label'=>'Diproses', 'color'=>'#9b59b6'],
|
||||
'selesai' => ['label'=>'Selesai', 'color'=>'#27ae72'],
|
||||
'ditolak' => ['label'=>'Ditolak', 'color'=>'#e85050'],
|
||||
];
|
||||
$urgensiLabel = [
|
||||
'darurat' => ['label'=>'🚨 Darurat', 'color'=>'#e85050'],
|
||||
'tinggi' => ['label'=>'⚠️ Tinggi', 'color'=>'#f0b429'],
|
||||
'sedang' => ['label'=>'🔵 Sedang', 'color'=>'#4a9eff'],
|
||||
'rendah' => ['label'=>'⬇️ Rendah', 'color'=>'#6b7e96'],
|
||||
];
|
||||
$kategoriLabel = [
|
||||
'kemiskinan' => 'Kemiskinan',
|
||||
'kesehatan' => 'Kesehatan',
|
||||
'pendidikan' => 'Pendidikan',
|
||||
'infrastruktur'=> 'Infrastruktur',
|
||||
'keamanan' => 'Keamanan',
|
||||
'lainnya' => 'Lainnya',
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Laporan Masyarakat — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;
|
||||
--border:#243044;--border2:#2f3f57;
|
||||
--text:#e8edf3;--muted:#6b7e96;
|
||||
--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--purple:#9b59b6;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;
|
||||
}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s,color .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(232,80,80,.12);color:var(--red);}
|
||||
.container{max-width:1400px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:6px;}
|
||||
|
||||
/* KPI */
|
||||
.kpi-row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:24px;}
|
||||
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 18px;flex:1;min-width:120px;cursor:pointer;transition:border-color .15s;}
|
||||
.kpi:hover{border-color:var(--border2);}
|
||||
.kpi.active{border-color:currentColor;}
|
||||
.kpi-num{font-family:var(--font-head);font-size:26px;font-weight:800;}
|
||||
.kpi-lbl{font-size:11px;color:var(--muted);margin-top:2px;}
|
||||
|
||||
/* Toolbar */
|
||||
.toolbar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;align-items:center;}
|
||||
.toolbar input[type=text]{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:8px;font-size:13px;width:220px;}
|
||||
.toolbar input[type=text]:focus{outline:none;border-color:var(--border2);}
|
||||
.toolbar select{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:8px;font-size:13px;}
|
||||
.btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:500;transition:opacity .15s;}
|
||||
.btn-primary{background:var(--blue);color:#fff;}
|
||||
.btn-sm{padding:5px 10px;font-size:12px;border-radius:6px;border:none;cursor:pointer;font-weight:500;}
|
||||
.btn-sm:hover{opacity:.85;}
|
||||
|
||||
/* Table */
|
||||
.table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
thead tr{background:var(--surface2);}
|
||||
th{padding:11px 14px;text-align:left;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;}
|
||||
td{padding:11px 14px;border-top:1px solid var(--border);vertical-align:top;}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600;}
|
||||
.desc-cell{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);}
|
||||
.kode{font-family:monospace;font-size:12px;color:var(--blue);}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:200;align-items:center;justify-content:center;}
|
||||
.modal-overlay.open{display:flex;}
|
||||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:16px;width:560px;max-width:95vw;max-height:90vh;overflow-y:auto;}
|
||||
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid var(--border);}
|
||||
.modal-head h3{font-family:var(--font-head);font-size:16px;font-weight:700;}
|
||||
.modal-close{background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;line-height:1;}
|
||||
.modal-body{padding:20px;}
|
||||
.detail-row{display:flex;gap:8px;margin-bottom:12px;font-size:13px;}
|
||||
.detail-key{color:var(--muted);min-width:140px;flex-shrink:0;}
|
||||
.detail-val{color:var(--text);word-break:break-word;}
|
||||
.section-title{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px;}
|
||||
.status-form{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;margin-top:12px;}
|
||||
.status-form select,.status-form textarea{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:8px;font-size:13px;}
|
||||
.status-form textarea{width:100%;min-height:60px;resize:vertical;}
|
||||
.foto-preview{width:100%;max-height:260px;object-fit:contain;border-radius:8px;border:1px solid var(--border);margin-bottom:10px;}
|
||||
|
||||
/* Empty */
|
||||
.empty{text-align:center;padding:48px 20px;color:var(--muted);}
|
||||
.empty-icon{font-size:40px;margin-bottom:10px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="laporan.php" class="nav-link active">Laporan</a>
|
||||
<a href="jadwal_piket.php" class="nav-link">Jadwal Piket</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted)">👤 <?= htmlspecialchars($_SESSION['nama']) ?></div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>📢 Laporan Masyarakat</h2>
|
||||
<p style="color:var(--muted);font-size:13px;margin-bottom:20px;">Kelola dan tindak lanjuti laporan yang masuk dari warga.</p>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="kpi-row">
|
||||
<?php
|
||||
$kpiData = [
|
||||
'' => ['label'=>'Semua', 'color'=>'var(--text)'],
|
||||
'pending' => ['label'=>'Pending', 'color'=>'#f0b429'],
|
||||
'diverifikasi'=>['label'=>'Diverifikasi', 'color'=>'#4a9eff'],
|
||||
'diproses' => ['label'=>'Diproses', 'color'=>'#9b59b6'],
|
||||
'selesai' => ['label'=>'Selesai', 'color'=>'#27ae72'],
|
||||
'ditolak' => ['label'=>'Ditolak', 'color'=>'#e85050'],
|
||||
];
|
||||
$total = array_sum($stats);
|
||||
foreach ($kpiData as $k => $v):
|
||||
$n = $k ? ($stats[$k] ?? 0) : $total;
|
||||
$active = $filterStatus === $k ? 'active' : '';
|
||||
?>
|
||||
<div class="kpi <?= $active ?>" style="color:<?= $v['color'] ?>" onclick="filterByStatus('<?= $k ?>')">
|
||||
<div class="kpi-num"><?= $n ?></div>
|
||||
<div class="kpi-lbl"><?= $v['label'] ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<form method="GET" class="toolbar">
|
||||
<input type="hidden" name="status" id="inp-status" value="<?= htmlspecialchars($filterStatus) ?>">
|
||||
<input type="text" name="q" placeholder="🔍 Cari kode / nama / deskripsi…" value="<?= htmlspecialchars($q) ?>">
|
||||
<select name="kategori">
|
||||
<option value="">Semua Kategori</option>
|
||||
<?php foreach ($kategoriLabel as $k=>$l): ?>
|
||||
<option value="<?=$k?>" <?= $filterKategori===$k?'selected':'' ?>><?= $l ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="urgensi">
|
||||
<option value="">Semua Urgensi</option>
|
||||
<?php foreach ($urgensiLabel as $k=>$v): ?>
|
||||
<option value="<?=$k?>" <?= $filterUrgensi===$k?'selected':'' ?>><?= $v['label'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Terapkan</button>
|
||||
<a href="laporan.php" style="font-size:13px;color:var(--muted);text-decoration:none;padding:8px 4px;">Reset</a>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-wrap">
|
||||
<?php if (empty($laporanList)): ?>
|
||||
<div class="empty">
|
||||
<div class="empty-icon">📭</div>
|
||||
<div>Belum ada laporan yang sesuai filter.</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kode</th>
|
||||
<th>Pelapor</th>
|
||||
<th>Kategori</th>
|
||||
<th>Urgensi</th>
|
||||
<th>Deskripsi</th>
|
||||
<th>Status</th>
|
||||
<th>Tanggal</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($laporanList as $row):
|
||||
$stInfo = $statusLabel[$row['status']] ?? ['label'=>$row['status'],'color'=>'#aaa'];
|
||||
$urInfo = $urgensiLabel[$row['urgensi']] ?? ['label'=>$row['urgensi'],'color'=>'#aaa'];
|
||||
$rowJson = htmlspecialchars(json_encode($row, JSON_UNESCAPED_UNICODE), ENT_QUOTES);
|
||||
?>
|
||||
<tr>
|
||||
<td><span class="kode"><?= htmlspecialchars($row['kode_laporan']) ?></span></td>
|
||||
<td><?= htmlspecialchars($row['nama_pelapor'] ?? $row['nama_penduduk'] ?? '—') ?></td>
|
||||
<td><?= htmlspecialchars($kategoriLabel[$row['kategori']] ?? $row['kategori']) ?></td>
|
||||
<td><span class="badge" style="background:<?= $urInfo['color'] ?>22;color:<?= $urInfo['color'] ?>"><?= $urInfo['label'] ?></span></td>
|
||||
<td class="desc-cell" title="<?= htmlspecialchars($row['deskripsi']) ?>"><?= htmlspecialchars($row['deskripsi']) ?></td>
|
||||
<td><span class="badge" style="background:<?= $stInfo['color'] ?>22;color:<?= $stInfo['color'] ?>"><?= $stInfo['label'] ?></span></td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= date('d/m/Y', strtotime($row['created_at'])) ?></td>
|
||||
<td>
|
||||
<button class="btn-sm" style="background:var(--surface3);color:var(--text)" onclick='openDetail(<?= $rowJson ?>)'>Detail</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p style="font-size:12px;color:var(--muted);margin-top:10px;">Menampilkan <?= count($laporanList) ?> laporan</p>
|
||||
</div>
|
||||
|
||||
<!-- Modal Detail -->
|
||||
<div class="modal-overlay" id="modal" onclick="closeModalOut(event)">
|
||||
<div class="modal" id="modal-box">
|
||||
<div class="modal-head">
|
||||
<h3 id="modal-title">Detail Laporan</h3>
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentId = null;
|
||||
|
||||
const statusLabel = <?= json_encode($statusLabel) ?>;
|
||||
const urgensiLabel = <?= json_encode($urgensiLabel) ?>;
|
||||
const kategoriLabel = <?= json_encode($kategoriLabel) ?>;
|
||||
|
||||
function filterByStatus(s) {
|
||||
document.getElementById('inp-status').value = s;
|
||||
document.querySelector('form.toolbar').submit();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('open');
|
||||
}
|
||||
function closeModalOut(e) {
|
||||
if (e.target === document.getElementById('modal')) closeModal();
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
currentId = row.id;
|
||||
document.getElementById('modal-title').textContent = 'Laporan ' + row.kode_laporan;
|
||||
|
||||
const ur = urgensiLabel[row.urgensi] || {label: row.urgensi, color: '#aaa'};
|
||||
const st = statusLabel[row.status] || {label: row.status, color: '#aaa'};
|
||||
const kat = kategoriLabel[row.kategori] || row.kategori;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (row.foto) {
|
||||
html += `<img class="foto-preview" src="uploads/${row.foto}" alt="Foto laporan">`;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
['Kode Laporan', `<span style="font-family:monospace;color:#4a9eff">${row.kode_laporan}</span>`],
|
||||
['Nama Pelapor', esc(row.nama_pelapor || row.nama_penduduk || '—')],
|
||||
['No. HP', esc(row.no_hp_pelapor || '—')],
|
||||
['Kategori', esc(kat)],
|
||||
['Urgensi', `<span class="badge" style="background:${ur.color}22;color:${ur.color}">${ur.label}</span>`],
|
||||
['Status', `<span class="badge" style="background:${st.color}22;color:${st.color}">${st.label}</span>`],
|
||||
['Alamat', esc(row.alamat_kejadian || '—')],
|
||||
['Deskripsi', `<span style="white-space:pre-wrap">${esc(row.deskripsi)}</span>`],
|
||||
['Tanggal Masuk', row.created_at ? row.created_at.substring(0,16) : '—'],
|
||||
];
|
||||
|
||||
if (row.catatan_verifikasi) fields.push(['Catatan Verifikasi', esc(row.catatan_verifikasi)]);
|
||||
if (row.nama_verifikator) fields.push(['Diverifikasi oleh', esc(row.nama_verifikator)]);
|
||||
if (row.nama_prosesor) fields.push(['Diproses oleh', esc(row.nama_prosesor)]);
|
||||
|
||||
fields.forEach(([k,v]) => {
|
||||
html += `<div class="detail-row"><div class="detail-key">${k}</div><div class="detail-val">${v}</div></div>`;
|
||||
});
|
||||
|
||||
<?php if (hasRole('admin','pengurus')): ?>
|
||||
html += `<div class="section-title">Update Status</div>
|
||||
<div class="status-form" id="status-form">
|
||||
<div style="display:flex;flex-direction:column;gap:6px;width:100%">
|
||||
<select id="new-status">
|
||||
${Object.entries(statusLabel).map(([k,v])=>`<option value="${k}" ${row.status===k?'selected':''}>${v.label}</option>`).join('')}
|
||||
</select>
|
||||
<textarea id="new-catatan" placeholder="Catatan (opsional)…">${esc(row.catatan_verifikasi || '')}</textarea>
|
||||
<button class="btn btn-primary" onclick="updateStatus()">Simpan Perubahan</button>
|
||||
</div>
|
||||
</div>`;
|
||||
<?php endif; ?>
|
||||
|
||||
document.getElementById('modal-body').innerHTML = html;
|
||||
document.getElementById('modal').classList.add('open');
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function updateStatus() {
|
||||
const status = document.getElementById('new-status').value;
|
||||
const catatan = document.getElementById('new-catatan').value;
|
||||
const fd = new FormData();
|
||||
fd.append('id', currentId);
|
||||
fd.append('status', status);
|
||||
fd.append('catatan', catatan);
|
||||
|
||||
fetch('api/laporan.php?action=update_status', { method:'POST', body:fd })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
closeModal();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Gagal: ' + (d.error || 'unknown'));
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Terjadi kesalahan jaringan.'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,402 @@
|
||||
<?php
|
||||
require_once 'koneksi.php';
|
||||
|
||||
if (isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = sanitize($conn, $_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
$user = dbFirst($conn, "SELECT u.*, r.slug as role_slug, r.nama as role_nama FROM users u JOIN roles r ON u.role_id=r.id WHERE u.username=? AND u.aktif=1", [$username], 's');
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['nama'] = $user['nama'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role_id'] = $user['role_id'];
|
||||
$_SESSION['role_slug'] = $user['role_slug'];
|
||||
$_SESSION['role_nama'] = $user['role_nama'];
|
||||
$_SESSION['ibadah_id'] = $user['rumah_ibadah_id'];
|
||||
$conn->query("UPDATE users SET last_login=NOW() WHERE id={$user['id']}");
|
||||
logAktivitas($conn, 'LOGIN', 'users', $user['id']);
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Username atau password salah.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login — WebGIS Pengentasan Kemiskinan</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c12;
|
||||
--surface:#0f1520;
|
||||
--surface2:#16202e;
|
||||
--surface3:#1d2b3e;
|
||||
--border:#253347;
|
||||
--border2:#2e3f57;
|
||||
--text:#edf2f9;
|
||||
--text2:#c4cfdf;
|
||||
--muted:#6b7e96;
|
||||
--muted2:#4e6278;
|
||||
--red:#e8455a;
|
||||
--red2:#ff6b7d;
|
||||
--green:#22c97d;
|
||||
--blue:#3d8ef0;
|
||||
--yellow:#f0b429;
|
||||
--purple:#9d6ef5;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;
|
||||
--font-body:'Inter',sans-serif;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family:var(--font-body);
|
||||
background:var(--bg);
|
||||
min-height:100vh;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
padding:20px;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
/* Background effects */
|
||||
.bg-grid{
|
||||
position:fixed;inset:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(61,142,240,0.04) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(61,142,240,0.04) 1px,transparent 1px);
|
||||
background-size:52px 52px;
|
||||
pointer-events:none;
|
||||
}
|
||||
.bg-glow{
|
||||
position:fixed;pointer-events:none;border-radius:50%;filter:blur(100px);
|
||||
}
|
||||
.glow1{width:500px;height:500px;background:radial-gradient(circle,rgba(232,69,90,0.18),transparent 70%);top:-150px;left:-150px;}
|
||||
.glow2{width:450px;height:450px;background:radial-gradient(circle,rgba(61,142,240,0.15),transparent 70%);bottom:-100px;right:-100px;}
|
||||
.glow3{width:300px;height:300px;background:radial-gradient(circle,rgba(157,110,245,0.1),transparent 70%);top:50%;left:50%;transform:translate(-50%,-50%);}
|
||||
|
||||
.wrapper{
|
||||
width:100%;max-width:480px;
|
||||
position:relative;z-index:1;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.card{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:28px;
|
||||
padding:44px 40px;
|
||||
box-shadow:0 40px 100px rgba(0,0,0,0.6),0 0 0 1px rgba(255,255,255,0.03) inset;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.logo{display:flex;align-items:center;gap:14px;margin-bottom:36px;}
|
||||
.logo-icon{
|
||||
width:50px;height:50px;
|
||||
background:linear-gradient(135deg,var(--red),var(--red2));
|
||||
border-radius:16px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:24px;
|
||||
box-shadow:0 8px 28px rgba(232,69,90,0.45);
|
||||
flex-shrink:0;
|
||||
}
|
||||
.logo-text{
|
||||
font-family:var(--font-head);
|
||||
font-size:15px;font-weight:800;
|
||||
color:var(--text);
|
||||
line-height:1.2;
|
||||
letter-spacing:-0.2px;
|
||||
}
|
||||
.logo-sub{font-size:11px;color:var(--muted);margin-top:4px;line-height:1.4;}
|
||||
|
||||
.heading{
|
||||
font-family:var(--font-head);
|
||||
font-size:30px;font-weight:800;
|
||||
color:var(--text);
|
||||
margin-bottom:6px;
|
||||
letter-spacing:-0.6px;
|
||||
line-height:1.2;
|
||||
}
|
||||
.subtitle{
|
||||
font-size:14px;
|
||||
color:var(--muted);
|
||||
margin-bottom:32px;
|
||||
font-weight:400;
|
||||
line-height:1.5;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-group{margin-bottom:18px;}
|
||||
label{
|
||||
display:block;
|
||||
font-size:11.5px;
|
||||
font-weight:600;
|
||||
color:var(--muted);
|
||||
text-transform:uppercase;
|
||||
letter-spacing:0.8px;
|
||||
margin-bottom:8px;
|
||||
}
|
||||
input{
|
||||
width:100%;
|
||||
background:var(--surface2);
|
||||
border:1px solid var(--border);
|
||||
border-radius:12px;
|
||||
padding:13px 16px;
|
||||
color:var(--text);
|
||||
font-family:var(--font-body);
|
||||
font-size:14.5px;
|
||||
outline:none;
|
||||
transition:border-color .2s,background .2s,box-shadow .2s;
|
||||
font-weight:400;
|
||||
}
|
||||
input:focus{
|
||||
border-color:var(--red);
|
||||
background:var(--surface3);
|
||||
box-shadow:0 0 0 3px rgba(232,69,90,0.12);
|
||||
}
|
||||
input::placeholder{color:var(--muted2);font-size:14px;}
|
||||
|
||||
.error-box{
|
||||
background:rgba(232,69,90,.1);
|
||||
border:1px solid rgba(232,69,90,.25);
|
||||
border-radius:10px;
|
||||
padding:11px 15px;
|
||||
color:#ff7a8a;
|
||||
font-size:13px;
|
||||
margin-bottom:20px;
|
||||
display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
|
||||
.btn-login{
|
||||
width:100%;
|
||||
padding:14px;
|
||||
background:linear-gradient(135deg,var(--red) 0%,#f05068 100%);
|
||||
color:white;border:none;
|
||||
border-radius:12px;
|
||||
font-family:var(--font-head);
|
||||
font-size:15px;font-weight:700;
|
||||
cursor:pointer;
|
||||
transition:opacity .2s,transform .15s,box-shadow .2s;
|
||||
box-shadow:0 6px 24px rgba(232,69,90,0.45);
|
||||
letter-spacing:-0.1px;
|
||||
}
|
||||
.btn-login:hover{opacity:.92;transform:translateY(-1px);box-shadow:0 8px 28px rgba(232,69,90,0.55);}
|
||||
.btn-login:active{transform:translateY(0);}
|
||||
|
||||
/* Divider */
|
||||
.divider{
|
||||
display:flex;align-items:center;gap:12px;
|
||||
margin:28px 0 18px;
|
||||
color:var(--muted2);
|
||||
font-size:11.5px;
|
||||
font-weight:500;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:0.6px;
|
||||
}
|
||||
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--border);}
|
||||
|
||||
/* Demo section label */
|
||||
.demo-label{
|
||||
font-family:var(--font-head);
|
||||
font-size:12px;font-weight:700;
|
||||
color:var(--muted);
|
||||
text-transform:uppercase;
|
||||
letter-spacing:0.8px;
|
||||
margin-bottom:12px;
|
||||
}
|
||||
.demo-sublabel{
|
||||
font-size:11.5px;color:var(--muted2);
|
||||
margin-bottom:14px;
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
/* Demo accounts grid */
|
||||
.demo-grid{
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:8px;
|
||||
}
|
||||
.demo-btn{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
background:var(--surface2);
|
||||
border:1px solid var(--border);
|
||||
border-radius:12px;
|
||||
padding:10px 12px;
|
||||
cursor:pointer;
|
||||
transition:border-color .2s,background .2s,transform .15s;
|
||||
text-align:left;
|
||||
color:var(--text);
|
||||
width:100%;
|
||||
}
|
||||
.demo-btn:hover{
|
||||
border-color:var(--border2);
|
||||
background:var(--surface3);
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
.demo-btn:active{transform:translateY(0);}
|
||||
|
||||
.demo-icon{
|
||||
width:34px;height:34px;
|
||||
border-radius:10px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:16px;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.demo-info{flex:1;min-width:0;}
|
||||
.demo-name{
|
||||
font-family:var(--font-head);
|
||||
font-size:12.5px;font-weight:700;
|
||||
color:var(--text);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||||
line-height:1.3;
|
||||
}
|
||||
.demo-role{font-size:10.5px;color:var(--muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.demo-user{
|
||||
font-size:10px;
|
||||
color:var(--muted2);
|
||||
font-family:'Courier New',monospace;
|
||||
background:rgba(255,255,255,0.04);
|
||||
padding:2px 6px;border-radius:5px;
|
||||
margin-top:3px;
|
||||
display:inline-block;
|
||||
letter-spacing:0.2px;
|
||||
}
|
||||
|
||||
.pass-info{
|
||||
margin-top:14px;
|
||||
text-align:center;
|
||||
font-size:11.5px;
|
||||
color:var(--muted2);
|
||||
}
|
||||
.pass-info span{
|
||||
color:var(--text2);
|
||||
font-family:'Courier New',monospace;
|
||||
background:rgba(255,255,255,0.06);
|
||||
padding:2px 8px;border-radius:5px;
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
@media(max-width:420px){
|
||||
.card{padding:32px 24px;}
|
||||
.demo-grid{grid-template-columns:1fr;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-grid"></div>
|
||||
<div class="bg-glow glow1"></div>
|
||||
<div class="bg-glow glow2"></div>
|
||||
<div class="bg-glow glow3"></div>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="card">
|
||||
<!-- Logo -->
|
||||
<div class="logo">
|
||||
<div class="logo-icon">🗺️</div>
|
||||
<div>
|
||||
<div class="logo-text">WebGIS Kemiskinan</div>
|
||||
<div class="logo-sub">Sistem Pengentasan Kemiskinan Berbasis Masyarakat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="heading">Masuk</h1>
|
||||
<p class="subtitle">Sistem Monitoring & Distribusi Bantuan Sosial</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-box">⚠️ <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" id="loginForm">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" id="uname" placeholder="Masukkan username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" id="upass" placeholder="Masukkan password" required autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn-login">Masuk ke Sistem →</button>
|
||||
</form>
|
||||
|
||||
<div class="divider">Demo Akun</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- Pimpinan -->
|
||||
<button class="demo-btn" onclick="fillLogin('pimpinan','password123')">
|
||||
<div class="demo-icon" style="background:rgba(61,142,240,0.18);">🏛️</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">Pimpinan</div>
|
||||
<div class="demo-role">Monitoring</div>
|
||||
<div class="demo-user">pimpinan</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Warga -->
|
||||
<button class="demo-btn" onclick="fillLogin('warga','password123')">
|
||||
<div class="demo-icon" style="background:rgba(240,180,41,0.18);">👤</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">Warga</div>
|
||||
<div class="demo-role">Masyarakat</div>
|
||||
<div class="demo-user">warga</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Pengurus 1 -->
|
||||
<button class="demo-btn" onclick="fillLogin('pengurus1','password123')">
|
||||
<div class="demo-icon" style="background:rgba(34,201,125,0.18);">🕌</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">Pengurus 1</div>
|
||||
<div class="demo-role">Input & Verifikasi</div>
|
||||
<div class="demo-user">pengurus1</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Pengurus 2 -->
|
||||
<button class="demo-btn" onclick="fillLogin('pengurus2','password123')">
|
||||
<div class="demo-icon" style="background:rgba(157,110,245,0.18);">🏠</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">Pengurus 2</div>
|
||||
<div class="demo-role">Input & Verifikasi</div>
|
||||
<div class="demo-user">pengurus2</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Admin (full width) -->
|
||||
<button class="demo-btn" onclick="fillLogin('admin','password123')" style="grid-column:1/-1;">
|
||||
<div class="demo-icon" style="background:rgba(232,69,90,0.18);">👑</div>
|
||||
<div class="demo-info">
|
||||
<div class="demo-name">Administrator</div>
|
||||
<div class="demo-role">Akses penuh sistem</div>
|
||||
<div class="demo-user">admin</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="pass-info">Password semua akun: <span>password123</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillLogin(u, p) {
|
||||
document.getElementById('uname').value = u;
|
||||
document.getElementById('upass').value = p;
|
||||
document.getElementById('loginForm').submit();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
require_once 'koneksi.php';
|
||||
if (isLoggedIn()) {
|
||||
logAktivitas($conn, 'LOGOUT', 'users', currentUserId());
|
||||
session_destroy();
|
||||
}
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// peristiwa_kependudukan.php — Kelola histori vital penduduk
|
||||
// Akses: pengurus, admin
|
||||
// ============================================================
|
||||
require_once 'koneksi.php';
|
||||
requireRole('admin','pengurus');
|
||||
|
||||
$msg = ''; $err = '';
|
||||
$role = getRole();
|
||||
$myRumahIbadahId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
|
||||
// Filter: pengurus hanya lihat wilayahnya
|
||||
$filterRumah = ($role === 'pengurus' && $myRumahIbadahId)
|
||||
? "AND k.rumah_ibadah_id = $myRumahIbadahId"
|
||||
: '';
|
||||
|
||||
// ---- HANDLE POST (simpan peristiwa baru) --------------------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['aksi'])) {
|
||||
if ($_POST['aksi'] === 'simpan') {
|
||||
$pid = intval($_POST['penduduk_id']);
|
||||
$jenis = $_POST['jenis'];
|
||||
$tgl = $_POST['tanggal'];
|
||||
$ket = trim($_POST['keterangan'] ?? '');
|
||||
$alamatAs = trim($_POST['alamat_asal'] ?? '');
|
||||
$alamatTj = trim($_POST['alamat_tujuan'] ?? '');
|
||||
$sebab = trim($_POST['sebab_meninggal'] ?? '');
|
||||
$dicatatOleh = currentUserId();
|
||||
|
||||
// Upload dokumen
|
||||
$dok = null;
|
||||
if (!empty($_FILES['dokumen']['name']))
|
||||
$dok = uploadFile($_FILES['dokumen'], 'peristiwa', ['jpg','jpeg','png','pdf']);
|
||||
|
||||
// Ambil keluarga_id dari penduduk
|
||||
$r = $conn->query("SELECT keluarga_id FROM penduduk WHERE id=$pid");
|
||||
$kidRow = $r->fetch_assoc();
|
||||
$kid = $kidRow['keluarga_id'] ?? 'NULL';
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO peristiwa_kependudukan
|
||||
(penduduk_id, keluarga_id, jenis, tanggal, keterangan,
|
||||
alamat_asal, alamat_tujuan, sebab_meninggal, dokumen, dicatat_oleh)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||||
$kidBind = $kidRow['keluarga_id'] ?: null;
|
||||
$sebabBind = $sebab ?: null;
|
||||
$asBind = $alamatAs ?: null;
|
||||
$tjBind = $alamatTj ?: null;
|
||||
$dokBind = $dok ?: null;
|
||||
$ketBind = $ket ?: null;
|
||||
$stmt->bind_param('iisssssssi',
|
||||
$pid, $kidBind, $jenis, $tgl, $ketBind,
|
||||
$asBind, $tjBind, $sebabBind, $dokBind, $dicatatOleh);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$newId = $conn->insert_id;
|
||||
logAktivitas($conn, 'CREATE_PERISTIWA', 'peristiwa_kependudukan', $newId);
|
||||
|
||||
// Sinkronisasi otomatis ke tabel penduduk sesuai jenis
|
||||
if ($jenis === 'meninggal') {
|
||||
$tglSan = sanitize($conn, $tgl);
|
||||
$conn->query("UPDATE penduduk SET status_hidup='meninggal', tanggal_meninggal='$tglSan', updated_by=$dicatatOleh WHERE id=$pid");
|
||||
} elseif ($jenis === 'menikah') {
|
||||
$conn->query("UPDATE penduduk SET status_kawin='kawin', updated_by=$dicatatOleh WHERE id=$pid");
|
||||
} elseif ($jenis === 'cerai') {
|
||||
$conn->query("UPDATE penduduk SET status_kawin='cerai_hidup', updated_by=$dicatatOleh WHERE id=$pid");
|
||||
} elseif ($jenis === 'pindah_keluar') {
|
||||
$conn->query("UPDATE penduduk SET keluarga_id=NULL, domisili_sama_kk=0, updated_by=$dicatatOleh WHERE id=$pid");
|
||||
}
|
||||
|
||||
$msg = 'Peristiwa berhasil dicatat.';
|
||||
} else {
|
||||
$err = 'Gagal menyimpan: ' . $conn->error;
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
} elseif ($_POST['aksi'] === 'verifikasi') {
|
||||
$id = intval($_POST['id']);
|
||||
$uid = currentUserId();
|
||||
$conn->query("UPDATE peristiwa_kependudukan SET diverifikasi=1, diverifikasi_oleh=$uid, diverifikasi_at=NOW() WHERE id=$id");
|
||||
logAktivitas($conn, 'VERIFIKASI_PERISTIWA', 'peristiwa_kependudukan', $id);
|
||||
$msg = 'Peristiwa berhasil diverifikasi.';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Load penduduk untuk dropdown -------------------------
|
||||
$pendudukList = $conn->query("
|
||||
SELECT p.id, p.nama, p.nik, k.alamat, ri.nama AS ibadah
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id = k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id = ri.id
|
||||
WHERE p.status_hidup='hidup' $filterRumah
|
||||
ORDER BY p.nama")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// ---- Load histori peristiwa --------------------------------
|
||||
$filter = $_GET['jenis'] ?? '';
|
||||
$search = sanitize($conn, $_GET['q'] ?? '');
|
||||
$wJenis = $filter ? "AND pk.jenis='$filter'" : '';
|
||||
$wSearch = $search ? "AND (p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')" : '';
|
||||
|
||||
$peristiwa = $conn->query("
|
||||
SELECT pk.*,
|
||||
p.nama AS nama_penduduk, p.nik,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
u.nama AS dicatat_nama,
|
||||
uv.nama AS diverifikasi_nama
|
||||
FROM peristiwa_kependudukan pk
|
||||
JOIN penduduk p ON pk.penduduk_id = p.id
|
||||
LEFT JOIN keluarga k ON p.keluarga_id = k.id
|
||||
LEFT JOIN users u ON pk.dicatat_oleh = u.id
|
||||
LEFT JOIN users uv ON pk.diverifikasi_oleh = uv.id
|
||||
WHERE 1=1 $filterRumah $wJenis $wSearch
|
||||
ORDER BY pk.tanggal DESC, pk.created_at DESC
|
||||
LIMIT 200")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$jenisLabel = [
|
||||
'lahir'=>'Kelahiran','meninggal'=>'Kematian','menikah'=>'Pernikahan',
|
||||
'cerai'=>'Perceraian','pindah_masuk'=>'Pindah Masuk','pindah_keluar'=>'Pindah Keluar',
|
||||
'ganti_nama'=>'Ganti Nama','perubahan_status_ekonomi'=>'Perubahan Status Ekonomi'
|
||||
];
|
||||
$jenisIcon = [
|
||||
'lahir'=>'👶','meninggal'=>'🕊️','menikah'=>'💍','cerai'=>'💔',
|
||||
'pindah_masuk'=>'🏠','pindah_keluar'=>'🚚','ganti_nama'=>'📝','perubahan_status_ekonomi'=>'📊'
|
||||
];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Peristiwa Kependudukan — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;--border:#243044;--border2:#2f3f57;--text:#e8edf3;--muted:#6b7e96;--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s,color .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(232,80,80,.12);color:var(--red);}
|
||||
.container{max-width:1200px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:22px;}
|
||||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap;}
|
||||
.search-box,.filter-sel{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:9px 14px;color:var(--text);font-family:var(--font-body);font-size:13px;outline:none;}
|
||||
.search-box{flex:1;min-width:200px;}.search-box:focus{border-color:var(--red);}
|
||||
.filter-sel option{background:var(--surface2);}
|
||||
.btn{padding:9px 18px;border:none;border-radius:10px;font-family:var(--font-body);font-size:13px;font-weight:600;cursor:pointer;transition:opacity .2s;}
|
||||
.btn:hover{opacity:.88;}.btn-primary{background:var(--red);color:#fff;box-shadow:0 3px 12px rgba(232,80,80,.3);}
|
||||
.btn-success{background:var(--green);color:#fff;}.btn-sm{padding:5px 12px;font-size:12px;border-radius:8px;}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;margin-bottom:28px;}
|
||||
.tbl-wrap{overflow-x:auto;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
th{text-align:left;padding:10px 14px;background:var(--surface2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border);}
|
||||
td{padding:10px 14px;border-bottom:1px solid var(--border2);vertical-align:middle;}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;}
|
||||
.b-verif{background:rgba(39,174,114,.2);color:var(--green);}
|
||||
.b-belum{background:rgba(107,126,150,.15);color:var(--muted);}
|
||||
.jenis-badge{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;background:var(--surface2);border:1px solid var(--border);}
|
||||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:500;align-items:center;justify-content:center;padding:20px;}
|
||||
.modal-bg.open{display:flex;}
|
||||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:20px;width:100%;max-width:580px;max-height:90vh;overflow-y:auto;}
|
||||
.modal-header{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);}
|
||||
.modal-title{font-family:var(--font-head);font-size:16px;font-weight:700;flex:1;}
|
||||
.modal-close{width:30px;height:30px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);font-size:16px;}
|
||||
.modal-body{padding:22px;}
|
||||
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
.form-full{grid-column:1/-1;}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;}
|
||||
.form-group label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:500;}
|
||||
.form-group input,.form-group select,.form-group textarea{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 13px;color:var(--text);font-family:var(--font-body);font-size:13.5px;outline:none;transition:border-color .2s;}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--red);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
.form-section{font-family:var(--font-head);font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;padding-bottom:8px;border-bottom:1px solid var(--border);grid-column:1/-1;margin-top:6px;}
|
||||
.form-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px;}
|
||||
.extra-field{display:none;}
|
||||
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
||||
.toast{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 16px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideIn .2s ease;min-width:260px;}
|
||||
.toast.success{border-left:3px solid var(--green);}.toast.error{border-left:3px solid var(--red);}
|
||||
@keyframes slideIn{from{transform:translateX(20px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
.empty{text-align:center;color:var(--muted);padding:40px;font-size:14px;}
|
||||
.alert{padding:12px 16px;border-radius:10px;font-size:13px;margin-bottom:16px;}
|
||||
.alert-ok{background:rgba(39,174,114,.12);border:1px solid rgba(39,174,114,.3);color:var(--green);}
|
||||
.alert-err{background:rgba(232,80,80,.15);border:1px solid rgba(232,80,80,.3);color:var(--red);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastCont"></div>
|
||||
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="keluarga.php" class="nav-link">Keluarga</a>
|
||||
<a href="peristiwa_kependudukan.php" class="nav-link active">Peristiwa</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted)">👤 <?= htmlspecialchars($_SESSION['nama']) ?></div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>📋 Peristiwa Kependudukan</h2>
|
||||
|
||||
<?php if ($msg): ?><div class="alert alert-ok">✅ <?= htmlspecialchars($msg) ?></div><?php endif; ?>
|
||||
<?php if ($err): ?><div class="alert alert-err">⚠️ <?= htmlspecialchars($err) ?></div><?php endif; ?>
|
||||
|
||||
<div class="toolbar">
|
||||
<form method="GET" style="display:contents">
|
||||
<input type="text" name="q" class="search-box" placeholder="🔍 Cari nama / NIK..." value="<?= htmlspecialchars($_GET['q']??'') ?>">
|
||||
<select name="jenis" class="filter-sel" onchange="this.form.submit()">
|
||||
<option value="">Semua Jenis</option>
|
||||
<?php foreach($jenisLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>" <?= ($filter===$v)?'selected':'' ?>><?=$jenisIcon[$v]?> <?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-outline">Cari</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" onclick="openModal()">+ Catat Peristiwa</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th><th>Penduduk</th><th>Jenis Peristiwa</th>
|
||||
<th>Keterangan</th><th>Dicatat Oleh</th><th>Status</th><th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($peristiwa)): ?>
|
||||
<tr><td colspan="7" class="empty">Belum ada data peristiwa.</td></tr>
|
||||
<?php else: foreach ($peristiwa as $p): ?>
|
||||
<tr>
|
||||
<td><?= date('d/m/Y', strtotime($p['tanggal'])) ?></td>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= htmlspecialchars($p['nama_penduduk']) ?></div>
|
||||
<div style="font-size:11px;color:var(--muted)"><?= htmlspecialchars($p['nik']) ?> · <?= $p['umur'] ?> th</div>
|
||||
</td>
|
||||
<td><span class="jenis-badge"><?= $jenisIcon[$p['jenis']]??'' ?> <?= $jenisLabel[$p['jenis']]??$p['jenis'] ?></span></td>
|
||||
<td style="max-width:240px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
<?= htmlspecialchars($p['keterangan'] ?: '-') ?>
|
||||
<?php if ($p['sebab_meninggal']): ?>
|
||||
<div style="font-size:11px;color:var(--muted)">Sebab: <?= htmlspecialchars($p['sebab_meninggal']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted)"><?= htmlspecialchars($p['dicatat_nama'] ?: 'Sistem') ?></td>
|
||||
<td>
|
||||
<?php if ($p['diverifikasi']): ?>
|
||||
<span class="badge b-verif">✓ Terverifikasi</span>
|
||||
<div style="font-size:10px;color:var(--muted)"><?= htmlspecialchars($p['diverifikasi_nama']) ?></div>
|
||||
<?php else: ?>
|
||||
<span class="badge b-belum">Belum diverifikasi</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!$p['diverifikasi'] && hasRole('admin','pengurus')): ?>
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="aksi" value="verifikasi">
|
||||
<input type="hidden" name="id" value="<?= $p['id'] ?>">
|
||||
<button type="submit" class="btn btn-success btn-sm" onclick="return confirm('Verifikasi peristiwa ini?')">✓ Verifikasi</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if ($p['dokumen']): ?>
|
||||
<a href="uploads/<?= htmlspecialchars($p['dokumen']) ?>" target="_blank" class="btn btn-outline btn-sm">📎 Dok</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Catat Peristiwa -->
|
||||
<div class="modal-bg" id="modalBg">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">📋 Catat Peristiwa Kependudukan</span>
|
||||
<div class="modal-close" onclick="closeModal()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="aksi" value="simpan">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Penduduk <span style="color:var(--red)">*</span></label>
|
||||
<select name="penduduk_id" required id="selPenduduk">
|
||||
<option value="">-- Pilih Penduduk --</option>
|
||||
<?php foreach ($pendudukList as $pd): ?>
|
||||
<option value="<?= $pd['id'] ?>"><?= htmlspecialchars($pd['nama']) ?> — <?= htmlspecialchars($pd['nik']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis Peristiwa <span style="color:var(--red)">*</span></label>
|
||||
<select name="jenis" required id="selJenis" onchange="toggleExtra(this.value)">
|
||||
<option value="">-- Pilih --</option>
|
||||
<?php foreach ($jenisLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$jenisIcon[$v]?> <?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal <span style="color:var(--red)">*</span></label>
|
||||
<input type="date" name="tanggal" required value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Keterangan</label>
|
||||
<textarea name="keterangan" rows="3" placeholder="Detail tambahan peristiwa..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Field khusus meninggal -->
|
||||
<div class="form-full form-group extra-field" id="ex_meninggal">
|
||||
<label>Sebab Meninggal</label>
|
||||
<input type="text" name="sebab_meninggal" placeholder="Sakit, kecelakaan, usia tua, dll">
|
||||
</div>
|
||||
|
||||
<!-- Field khusus pindah -->
|
||||
<div class="form-group extra-field" id="ex_pindah_as">
|
||||
<label>Alamat Asal</label>
|
||||
<input type="text" name="alamat_asal" placeholder="Alamat sebelum pindah">
|
||||
</div>
|
||||
<div class="form-group extra-field" id="ex_pindah_tj">
|
||||
<label>Alamat Tujuan</label>
|
||||
<input type="text" name="alamat_tujuan" placeholder="Alamat tujuan pindah">
|
||||
</div>
|
||||
|
||||
<div class="form-full form-group">
|
||||
<label>Dokumen Pendukung (opsional)</label>
|
||||
<input type="file" name="dokumen" accept=".jpg,.jpeg,.png,.pdf" style="padding:8px">
|
||||
<span style="font-size:11px;color:var(--muted)">Scan surat keterangan RT, akta, dll. Max 5MB.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModal()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">💾 Simpan Peristiwa</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openModal(){ document.getElementById('modalBg').classList.add('open'); }
|
||||
function closeModal(){ document.getElementById('modalBg').classList.remove('open'); }
|
||||
document.getElementById('modalBg').addEventListener('click', function(e){ if(e.target===this) closeModal(); });
|
||||
|
||||
function toggleExtra(jenis) {
|
||||
['ex_meninggal','ex_pindah_as','ex_pindah_tj'].forEach(id => {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
});
|
||||
if (jenis === 'meninggal') {
|
||||
document.getElementById('ex_meninggal').style.display = 'flex';
|
||||
} else if (jenis === 'pindah_masuk' || jenis === 'pindah_keluar') {
|
||||
document.getElementById('ex_pindah_as').style.display = 'flex';
|
||||
document.getElementById('ex_pindah_tj').style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
<?php if ($msg): ?>
|
||||
(function(){ var t=document.createElement('div'); t.className='toast success'; t.textContent='✅ <?= addslashes($msg) ?>'; document.getElementById('toastCont').appendChild(t); setTimeout(()=>t.remove(),3500); })();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// rekomendasi_pelatihan.php — Kelola & tampilkan rekomendasi
|
||||
// pelatihan per penduduk berdasarkan umur & status ekonomi
|
||||
// Akses: admin, pengurus
|
||||
// ============================================================
|
||||
require_once 'koneksi.php';
|
||||
requireRole('admin','pengurus');
|
||||
|
||||
$msg = ''; $err = '';
|
||||
$role = getRole();
|
||||
$myRumahIbadahId = $_SESSION['rumah_ibadah_id'] ?? null;
|
||||
$filterRumah = ($role === 'pengurus' && $myRumahIbadahId)
|
||||
? "AND k.rumah_ibadah_id = $myRumahIbadahId" : '';
|
||||
|
||||
// ---- HANDLE POST --------------------------------------------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$aksi = $_POST['aksi'] ?? '';
|
||||
|
||||
// Tambah master pelatihan baru
|
||||
if ($aksi === 'tambah_master') {
|
||||
$nama = sanitize($conn, trim($_POST['nama']));
|
||||
$kat = $_POST['kategori'];
|
||||
$desk = sanitize($conn, trim($_POST['deskripsi'] ?? ''));
|
||||
$penye = sanitize($conn, trim($_POST['penyelenggara'] ?? ''));
|
||||
$kontak = sanitize($conn, trim($_POST['kontak_penyelenggara'] ?? ''));
|
||||
$umin = intval($_POST['usia_min']);
|
||||
$umaks = intval($_POST['usia_maks']);
|
||||
$gender = $_POST['gender'];
|
||||
$sasaran = $_POST['status_ekonomi_sasaran'];
|
||||
$prsyrt = sanitize($conn, trim($_POST['prasyarat'] ?? ''));
|
||||
$durasi = sanitize($conn, trim($_POST['durasi'] ?? ''));
|
||||
$biaya = floatval($_POST['biaya'] ?? 0);
|
||||
|
||||
$conn->query("INSERT INTO rekomendasi_pelatihan
|
||||
(nama,kategori,deskripsi,penyelenggara,kontak_penyelenggara,
|
||||
usia_min,usia_maks,gender,status_ekonomi_sasaran,prasyarat,durasi,biaya)
|
||||
VALUES ('$nama','$kat','$desk','$penye','$kontak',
|
||||
$umin,$umaks,'$gender','$sasaran','$prsyrt','$durasi',$biaya)");
|
||||
logAktivitas($conn,'CREATE_REKOMENDASI_PELATIHAN','rekomendasi_pelatihan',$conn->insert_id);
|
||||
$msg = "Pelatihan '$nama' berhasil ditambahkan.";
|
||||
|
||||
// Rekomendasikan pelatihan ke penduduk tertentu
|
||||
} elseif ($aksi === 'rekomendasikan') {
|
||||
$pid = intval($_POST['penduduk_id']);
|
||||
$rekId = intval($_POST['rekomendasi_pelatihan_id']);
|
||||
$catatan= sanitize($conn, trim($_POST['catatan'] ?? ''));
|
||||
$oleh = currentUserId();
|
||||
$tgl = date('Y-m-d');
|
||||
|
||||
// Cek duplikat
|
||||
$cek = $conn->query("SELECT id FROM penduduk_pelatihan
|
||||
WHERE penduduk_id=$pid AND rekomendasi_pelatihan_id=$rekId
|
||||
AND status NOT IN ('selesai','batal')")->fetch_assoc();
|
||||
if ($cek) {
|
||||
$err = 'Pelatihan ini sudah pernah direkomendasikan ke penduduk tersebut dan masih aktif.';
|
||||
} else {
|
||||
$conn->query("INSERT INTO penduduk_pelatihan
|
||||
(penduduk_id,rekomendasi_pelatihan_id,status,tanggal_rekomendasi,catatan,direkomendasikan_oleh)
|
||||
VALUES ($pid,$rekId,'direkomendasikan','$tgl','$catatan',$oleh)");
|
||||
// Kirim notifikasi ke pengurus lain
|
||||
logAktivitas($conn,'REKOMENDASIKAN_PELATIHAN','penduduk_pelatihan',$conn->insert_id);
|
||||
$msg = 'Rekomendasi berhasil dikirim.';
|
||||
}
|
||||
|
||||
// Update status pelatihan (mendaftar, berjalan, selesai, batal)
|
||||
} elseif ($aksi === 'update_status') {
|
||||
$id = intval($_POST['id']);
|
||||
$status = $_POST['status'];
|
||||
$tMulai = sanitize($conn, $_POST['tanggal_mulai'] ?? '');
|
||||
$tSls = sanitize($conn, $_POST['tanggal_selesai'] ?? '');
|
||||
$sertif = null;
|
||||
if (!empty($_FILES['sertifikat']['name']))
|
||||
$sertif = uploadFile($_FILES['sertifikat'], 'sertif', ['jpg','jpeg','png','pdf']);
|
||||
|
||||
$setMulai = $tMulai ? ", tanggal_mulai='$tMulai'" : '';
|
||||
$setSelesai = $tSls ? ", tanggal_selesai='$tSls'" : '';
|
||||
$setSertif = $sertif ? ", sertifikat='$sertif'" : '';
|
||||
$conn->query("UPDATE penduduk_pelatihan
|
||||
SET status='$status' $setMulai $setSelesai $setSertif
|
||||
WHERE id=$id");
|
||||
logAktivitas($conn,'UPDATE_STATUS_PELATIHAN','penduduk_pelatihan',$id);
|
||||
$msg = 'Status pelatihan diperbarui.';
|
||||
|
||||
// Toggle aktif/nonaktif master pelatihan
|
||||
} elseif ($aksi === 'toggle_aktif') {
|
||||
$id = intval($_POST['id']);
|
||||
$val = intval($_POST['aktif']);
|
||||
$conn->query("UPDATE rekomendasi_pelatihan SET aktif=$val WHERE id=$id");
|
||||
$msg = $val ? 'Pelatihan diaktifkan.' : 'Pelatihan dinonaktifkan.';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Load data ----------------------------------------------
|
||||
$tab = $_GET['tab'] ?? 'penduduk'; // tab: penduduk | master
|
||||
|
||||
// Master pelatihan
|
||||
$masterList = $conn->query("SELECT * FROM rekomendasi_pelatihan ORDER BY kategori,nama")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Rekap per penduduk: semua yang punya rekomendasi / otomatis kebutuhan
|
||||
$search = sanitize($conn, $_GET['q'] ?? '');
|
||||
$wSearch = $search ? "AND (p.nama LIKE '%$search%' OR p.nik LIKE '%$search%')" : '';
|
||||
$filterStatus = sanitize($conn, $_GET['status'] ?? '');
|
||||
$wStatus = $filterStatus ? "AND pp.status='$filterStatus'" : '';
|
||||
|
||||
$rekPerPenduduk = $conn->query("
|
||||
SELECT p.id AS pid, p.nama, p.nik,
|
||||
TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
p.jenis_kelamin, p.status_ekonomi, p.kondisi_kesehatan,
|
||||
p.kebutuhan_pelatihan,
|
||||
k.alamat, ri.nama AS ibadah,
|
||||
pp.id AS pp_id, pp.status AS pp_status,
|
||||
pp.tanggal_rekomendasi, pp.tanggal_mulai, pp.tanggal_selesai,
|
||||
pp.sertifikat, pp.catatan,
|
||||
rp.nama AS nama_pelatihan, rp.kategori, rp.penyelenggara,
|
||||
rp.durasi, rp.biaya,
|
||||
u.nama AS direkomendasikan_oleh
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
LEFT JOIN rumah_ibadah ri ON k.rumah_ibadah_id=ri.id
|
||||
LEFT JOIN penduduk_pelatihan pp ON pp.penduduk_id=p.id
|
||||
LEFT JOIN rekomendasi_pelatihan rp ON pp.rekomendasi_pelatihan_id=rp.id
|
||||
LEFT JOIN users u ON pp.direkomendasikan_oleh=u.id
|
||||
WHERE p.status_hidup='hidup' $filterRumah $wSearch $wStatus
|
||||
ORDER BY p.nama, pp.tanggal_rekomendasi DESC
|
||||
LIMIT 300")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Penduduk untuk dropdown rekomendasikan
|
||||
$pendudukDrop = $conn->query("
|
||||
SELECT p.id, p.nama, p.nik,
|
||||
TIMESTAMPDIFF(YEAR,p.tanggal_lahir,CURDATE()) AS umur,
|
||||
p.status_ekonomi, p.jenis_kelamin
|
||||
FROM penduduk p
|
||||
LEFT JOIN keluarga k ON p.keluarga_id=k.id
|
||||
WHERE p.status_hidup='hidup' $filterRumah
|
||||
ORDER BY p.nama")->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$katLabel = ['vokasi'=>'Vokasi','kewirausahaan'=>'Kewirausahaan','digital'=>'Digital',
|
||||
'pertanian'=>'Pertanian','kesehatan'=>'Kesehatan','pendidikan'=>'Pendidikan','lainnya'=>'Lainnya'];
|
||||
$katColor = ['vokasi'=>'var(--blue)','kewirausahaan'=>'var(--green)','digital'=>'var(--purple,#9b59b6)',
|
||||
'pertanian'=>'#27ae72','kesehatan'=>'var(--red)','pendidikan'=>'var(--yellow)','lainnya'=>'var(--muted)'];
|
||||
$statusLabel = ['direkomendasikan'=>'Direkomendasikan','mendaftar'=>'Mendaftar',
|
||||
'sedang_berjalan'=>'Sedang Berjalan','selesai'=>'Selesai','batal'=>'Batal'];
|
||||
$statusColor = ['direkomendasikan'=>'var(--yellow)','mendaftar'=>'var(--blue)',
|
||||
'sedang_berjalan'=>'var(--green)','selesai'=>'#27ae72','batal'=>'var(--muted)'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Rekomendasi Pelatihan — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--surface3:#1e2a3a;--border:#243044;--border2:#2f3f57;--text:#e8edf3;--muted:#6b7e96;--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;--purple:#9b59b6;--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);min-height:100vh;}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(74,158,255,.12);color:var(--blue);}
|
||||
.container{max-width:1200px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:6px;}
|
||||
.tabs{display:flex;gap:6px;margin-bottom:22px;border-bottom:1px solid var(--border);padding-bottom:0;}
|
||||
.tab-btn{padding:9px 18px;border:none;background:transparent;color:var(--muted);font-family:var(--font-body);font-size:13px;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s;}
|
||||
.tab-btn.active{color:var(--blue);border-bottom-color:var(--blue);}
|
||||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap;}
|
||||
.search-box,.filter-sel{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:9px 14px;color:var(--text);font-family:var(--font-body);font-size:13px;outline:none;}
|
||||
.search-box{flex:1;min-width:180px;}.search-box:focus{border-color:var(--blue);}
|
||||
.filter-sel option{background:var(--surface2);}
|
||||
.btn{padding:9px 18px;border:none;border-radius:10px;font-family:var(--font-body);font-size:13px;font-weight:600;cursor:pointer;transition:opacity .2s;}
|
||||
.btn:hover{opacity:.88;}.btn-primary{background:var(--blue);color:#fff;}
|
||||
.btn-success{background:var(--green);color:#fff;}.btn-sm{padding:5px 12px;font-size:12px;border-radius:8px;}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
|
||||
.btn-danger{background:var(--red);color:#fff;}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;margin-bottom:24px;}
|
||||
.tbl-wrap{overflow-x:auto;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
th{text-align:left;padding:10px 14px;background:var(--surface2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border);}
|
||||
td{padding:10px 14px;border-bottom:1px solid var(--border2);vertical-align:middle;}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:10px;font-weight:600;}
|
||||
.kebutuhan-tag{display:inline-block;background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:2px 8px;font-size:10px;margin:2px 2px 2px 0;color:var(--muted);}
|
||||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:500;align-items:center;justify-content:center;padding:20px;}
|
||||
.modal-bg.open{display:flex;}
|
||||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:20px;width:100%;max-width:580px;max-height:90vh;overflow-y:auto;}
|
||||
.modal-header{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);}
|
||||
.modal-title{font-family:var(--font-head);font-size:16px;font-weight:700;flex:1;}
|
||||
.modal-close{width:30px;height:30px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);font-size:16px;}
|
||||
.modal-body{padding:22px;}
|
||||
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
||||
.form-full{grid-column:1/-1;}
|
||||
.form-group{display:flex;flex-direction:column;gap:6px;}
|
||||
.form-group label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;font-weight:500;}
|
||||
.form-group input,.form-group select,.form-group textarea{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:10px 13px;color:var(--text);font-family:var(--font-body);font-size:13.5px;outline:none;transition:border-color .2s;}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--blue);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
.form-section{font-family:var(--font-head);font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;padding-bottom:8px;border-bottom:1px solid var(--border);grid-column:1/-1;margin-top:6px;}
|
||||
.form-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px;}
|
||||
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
||||
.toast{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 16px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideIn .2s ease;min-width:260px;}
|
||||
.toast.success{border-left:3px solid var(--green);}.toast.error{border-left:3px solid var(--red);}
|
||||
@keyframes slideIn{from{transform:translateX(20px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
.empty{text-align:center;color:var(--muted);padding:40px;font-size:14px;}
|
||||
.alert{padding:12px 16px;border-radius:10px;font-size:13px;margin-bottom:16px;}
|
||||
.alert-ok{background:rgba(39,174,114,.12);border:1px solid rgba(39,174,114,.3);color:var(--green);}
|
||||
.alert-err{background:rgba(232,80,80,.15);border:1px solid rgba(232,80,80,.3);color:var(--red);}
|
||||
.master-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:16px;display:flex;flex-direction:column;gap:8px;}
|
||||
.master-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px;}
|
||||
.mc-head{display:flex;align-items:flex-start;gap:10px;}
|
||||
.mc-title{font-weight:600;font-size:14px;flex:1;}
|
||||
.mc-meta{font-size:11px;color:var(--muted);display:flex;gap:10px;flex-wrap:wrap;}
|
||||
.mc-actions{display:flex;gap:6px;margin-top:4px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastCont"></div>
|
||||
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="keluarga.php" class="nav-link">Keluarga</a>
|
||||
<a href="rekomendasi_pelatihan.php" class="nav-link active">Pelatihan</a>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--muted)">👤 <?= htmlspecialchars($_SESSION['nama']) ?></div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>🎓 Rekomendasi Pelatihan</h2>
|
||||
<p style="color:var(--muted);font-size:13px;margin-bottom:18px;">Kelola program pelatihan & rekomendasikan ke penduduk berdasarkan umur dan kondisi ekonomi.</p>
|
||||
|
||||
<?php if ($msg): ?><div class="alert alert-ok">✅ <?= htmlspecialchars($msg) ?></div><?php endif; ?>
|
||||
<?php if ($err): ?><div class="alert alert-err">⚠️ <?= htmlspecialchars($err) ?></div><?php endif; ?>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn <?= $tab==='penduduk'?'active':'' ?>" onclick="switchTab('penduduk')">👥 Per Penduduk</button>
|
||||
<button class="tab-btn <?= $tab==='master'?'active':'' ?>" onclick="switchTab('master')">📚 Master Pelatihan</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: PER PENDUDUK -->
|
||||
<div id="tabPenduduk" style="display:<?= $tab==='penduduk'?'block':'none' ?>">
|
||||
<div class="toolbar">
|
||||
<form method="GET" style="display:contents">
|
||||
<input type="hidden" name="tab" value="penduduk">
|
||||
<input type="text" name="q" class="search-box" placeholder="🔍 Cari nama / NIK..." value="<?= htmlspecialchars($_GET['q']??'') ?>">
|
||||
<select name="status" class="filter-sel" onchange="this.form.submit()">
|
||||
<option value="">Semua Status</option>
|
||||
<?php foreach ($statusLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>" <?= ($filterStatus===$v)?'selected':'' ?>><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-outline">Cari</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" onclick="openModalRek()">+ Rekomendasikan</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Penduduk</th><th>Umur / Status</th><th>Kebutuhan Otomatis</th>
|
||||
<th>Pelatihan Direkomendasikan</th><th>Status</th><th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
// Group by penduduk
|
||||
$grouped = [];
|
||||
foreach ($rekPerPenduduk as $r) {
|
||||
$grouped[$r['pid']] = $grouped[$r['pid']] ?? ['info'=>$r, 'pelatihan'=>[]];
|
||||
if ($r['pp_id']) $grouped[$r['pid']]['pelatihan'][] = $r;
|
||||
}
|
||||
if (empty($grouped)): ?>
|
||||
<tr><td colspan="6" class="empty">Belum ada data. Klik "+ Rekomendasikan" untuk mulai.</td></tr>
|
||||
<?php else: foreach ($grouped as $gd):
|
||||
$info = $gd['info'];
|
||||
$pelatihan = $gd['pelatihan'];
|
||||
$kebutuhan = $info['kebutuhan_pelatihan'] ? json_decode($info['kebutuhan_pelatihan'], true) : [];
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-weight:600"><?= htmlspecialchars($info['nama']) ?></div>
|
||||
<div style="font-size:11px;color:var(--muted)"><?= htmlspecialchars($info['nik']) ?></div>
|
||||
<div style="font-size:11px;color:var(--muted)"><?= htmlspecialchars($info['ibadah']??'-') ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= $info['umur'] ?> tahun · <?= $info['jenis_kelamin']==='L'?'Laki-laki':'Perempuan' ?></div>
|
||||
<div>
|
||||
<span class="badge" style="background:rgba(232,80,80,.15);color:var(--red)"><?= ucfirst($info['status_ekonomi']) ?></span>
|
||||
<?php if ($info['kondisi_kesehatan'] !== 'sehat'): ?>
|
||||
<span class="badge" style="background:rgba(240,180,41,.15);color:var(--yellow)"><?= ucfirst(str_replace('_',' ',$info['kondisi_kesehatan'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($kebutuhan): foreach ($kebutuhan as $kb): ?>
|
||||
<span class="kebutuhan-tag">💡 <?= htmlspecialchars($kb) ?></span>
|
||||
<?php endforeach; else: ?>
|
||||
<span style="font-size:11px;color:var(--muted)">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (empty($pelatihan)): ?>
|
||||
<span style="font-size:12px;color:var(--muted)">Belum ada rekomendasi</span>
|
||||
<?php else: foreach ($pelatihan as $pt): ?>
|
||||
<div style="margin-bottom:5px;">
|
||||
<div style="font-size:12px;font-weight:600"><?= htmlspecialchars($pt['nama_pelatihan']) ?></div>
|
||||
<div style="font-size:10px;color:var(--muted)"><?= htmlspecialchars($pt['penyelenggara']??'-') ?> · <?= htmlspecialchars($pt['durasi']??'-') ?></div>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (empty($pelatihan)): ?>
|
||||
<span class="badge" style="background:rgba(107,126,150,.15);color:var(--muted)">Belum</span>
|
||||
<?php else: foreach ($pelatihan as $pt): ?>
|
||||
<span class="badge" style="background:rgba(255,255,255,.06);color:<?= $statusColor[$pt['pp_status']]??'var(--muted)' ?>">
|
||||
<?= $statusLabel[$pt['pp_status']]??$pt['pp_status'] ?>
|
||||
</span><br>
|
||||
<?php endforeach; endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($pelatihan)): foreach ($pelatihan as $pt): ?>
|
||||
<?php if ($pt['pp_status'] !== 'selesai' && $pt['pp_status'] !== 'batal'): ?>
|
||||
<button class="btn btn-success btn-sm" style="margin-bottom:3px"
|
||||
onclick="openModalStatus(<?= $pt['pp_id'] ?>,'<?= $pt['pp_status'] ?>')">
|
||||
✏️ Update
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($pt['sertifikat']): ?>
|
||||
<a href="uploads/<?= htmlspecialchars($pt['sertifikat']) ?>" target="_blank" class="btn btn-outline btn-sm">🏆 Sertifikat</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: MASTER PELATIHAN -->
|
||||
<div id="tabMaster" style="display:<?= $tab==='master'?'block':'none' ?>">
|
||||
<div class="toolbar">
|
||||
<span style="font-size:13px;color:var(--muted)"><?= count($masterList) ?> program terdaftar</span>
|
||||
<button class="btn btn-primary" onclick="openModalMaster()">+ Tambah Program</button>
|
||||
</div>
|
||||
<div class="master-grid">
|
||||
<?php foreach ($masterList as $m): ?>
|
||||
<div class="master-card" style="opacity:<?= $m['aktif']?'1':'.5' ?>">
|
||||
<div class="mc-head">
|
||||
<div>
|
||||
<span class="badge" style="background:rgba(74,158,255,.15);color:var(--blue);margin-bottom:6px"><?= ucfirst($m['kategori']) ?></span>
|
||||
<div class="mc-title"><?= htmlspecialchars($m['nama']) ?></div>
|
||||
</div>
|
||||
<?php if (!$m['aktif']): ?><span class="badge" style="background:rgba(107,126,150,.15);color:var(--muted)">Nonaktif</span><?php endif; ?>
|
||||
</div>
|
||||
<div class="mc-meta">
|
||||
<span>👥 <?= $m['usia_min'] ?>–<?= $m['usia_maks'] ?> th</span>
|
||||
<span>⚥ <?= ucfirst($m['gender']) ?></span>
|
||||
<span>⏱ <?= htmlspecialchars($m['durasi']??'-') ?></span>
|
||||
<span>💰 <?= $m['biaya']>0?'Rp '.number_format($m['biaya'],0,',','.'):'Gratis' ?></span>
|
||||
</div>
|
||||
<?php if ($m['penyelenggara']): ?>
|
||||
<div style="font-size:12px;color:var(--muted)">🏛 <?= htmlspecialchars($m['penyelenggara']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($m['prasyarat']): ?>
|
||||
<div style="font-size:11px;color:var(--yellow)">⚠️ <?= htmlspecialchars($m['prasyarat']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="mc-actions">
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="aksi" value="toggle_aktif">
|
||||
<input type="hidden" name="id" value="<?= $m['id'] ?>">
|
||||
<input type="hidden" name="aktif" value="<?= $m['aktif']?0:1 ?>">
|
||||
<button type="submit" class="btn btn-outline btn-sm"><?= $m['aktif']?'⏸ Nonaktifkan':'▶️ Aktifkan' ?></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Rekomendasikan ke Penduduk -->
|
||||
<div class="modal-bg" id="modalRek">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">🎯 Rekomendasikan Pelatihan</span>
|
||||
<div class="modal-close" onclick="closeModalRek()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="aksi" value="rekomendasikan">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Penduduk <span style="color:var(--red)">*</span></label>
|
||||
<select name="penduduk_id" required id="selPendudukRek">
|
||||
<option value="">-- Pilih Penduduk --</option>
|
||||
<?php foreach ($pendudukDrop as $pd): ?>
|
||||
<option value="<?=$pd['id']?>"><?= htmlspecialchars($pd['nama']) ?> (<?=$pd['umur']?> th · <?= ucfirst($pd['status_ekonomi']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Program Pelatihan <span style="color:var(--red)">*</span></label>
|
||||
<select name="rekomendasi_pelatihan_id" required>
|
||||
<option value="">-- Pilih Program --</option>
|
||||
<?php foreach ($masterList as $m): if (!$m['aktif']) continue; ?>
|
||||
<option value="<?=$m['id']?>">[<?= ucfirst($m['kategori']) ?>] <?= htmlspecialchars($m['nama']) ?> — <?= htmlspecialchars($m['durasi']??'-') ?> <?= $m['biaya']>0?'(Rp '.number_format($m['biaya'],0,',','.').')':'(Gratis)' ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Catatan</label>
|
||||
<textarea name="catatan" rows="2" placeholder="Alasan rekomendasi, kebutuhan khusus, dll"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalRek()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">✅ Rekomendasikan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Update Status -->
|
||||
<div class="modal-bg" id="modalStatus">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">✏️ Update Status Pelatihan</span>
|
||||
<div class="modal-close" onclick="closeModalStatus()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="aksi" value="update_status">
|
||||
<input type="hidden" name="id" id="statusId">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Status Baru <span style="color:var(--red)">*</span></label>
|
||||
<select name="status" id="statusSel" required>
|
||||
<?php foreach ($statusLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal Mulai</label>
|
||||
<input type="date" name="tanggal_mulai">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal Selesai</label>
|
||||
<input type="date" name="tanggal_selesai">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Upload Sertifikat (jika sudah selesai)</label>
|
||||
<input type="file" name="sertifikat" accept=".jpg,.jpeg,.png,.pdf" style="padding:8px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalStatus()">Batal</button>
|
||||
<button type="submit" class="btn btn-success">💾 Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Tambah Master -->
|
||||
<div class="modal-bg" id="modalMaster">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">📚 Tambah Program Pelatihan</span>
|
||||
<div class="modal-close" onclick="closeModalMaster()">✕</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="aksi" value="tambah_master">
|
||||
<div class="form-grid">
|
||||
<div class="form-full form-group">
|
||||
<label>Nama Program <span style="color:var(--red)">*</span></label>
|
||||
<input type="text" name="nama" required placeholder="Misal: Pelatihan Sablon Kaos">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kategori <span style="color:var(--red)">*</span></label>
|
||||
<select name="kategori" required>
|
||||
<?php foreach ($katLabel as $v=>$l): ?>
|
||||
<option value="<?=$v?>"><?=$l?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Gender Sasaran</label>
|
||||
<select name="gender">
|
||||
<option value="semua">Semua</option>
|
||||
<option value="L">Laki-laki</option>
|
||||
<option value="P">Perempuan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Usia Min</label>
|
||||
<input type="number" name="usia_min" value="15" min="5" max="99">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Usia Maks</label>
|
||||
<input type="number" name="usia_maks" value="60" min="5" max="99">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sasaran Ekonomi</label>
|
||||
<select name="status_ekonomi_sasaran">
|
||||
<option value="semua">Semua</option>
|
||||
<option value="miskin">Miskin</option>
|
||||
<option value="rentan">Rentan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Durasi</label>
|
||||
<input type="text" name="durasi" placeholder="3 hari, 2 minggu...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Biaya (Rp, 0=gratis)</label>
|
||||
<input type="number" name="biaya" value="0" min="0">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Penyelenggara</label>
|
||||
<input type="text" name="penyelenggara" placeholder="BLK, Dinas Sosial, NGO, Masjid...">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Kontak Penyelenggara</label>
|
||||
<input type="text" name="kontak_penyelenggara" placeholder="No. HP / email">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Prasyarat</label>
|
||||
<input type="text" name="prasyarat" placeholder="Punya motor & SIM C, punya smartphone, dll">
|
||||
</div>
|
||||
<div class="form-full form-group">
|
||||
<label>Deskripsi</label>
|
||||
<textarea name="deskripsi" rows="3" placeholder="Deskripsi singkat program pelatihan..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-outline" onclick="closeModalMaster()">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">💾 Tambah Program</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTab(t) {
|
||||
document.getElementById('tabPenduduk').style.display = t==='penduduk'?'block':'none';
|
||||
document.getElementById('tabMaster').style.display = t==='master'?'block':'none';
|
||||
document.querySelectorAll('.tab-btn').forEach((b,i)=>b.classList.toggle('active',i===(t==='penduduk'?0:1)));
|
||||
}
|
||||
function openModalRek(){ document.getElementById('modalRek').classList.add('open'); }
|
||||
function closeModalRek(){ document.getElementById('modalRek').classList.remove('open'); }
|
||||
function openModalMaster(){ document.getElementById('modalMaster').classList.add('open'); }
|
||||
function closeModalMaster(){ document.getElementById('modalMaster').classList.remove('open'); }
|
||||
function openModalStatus(id, status) {
|
||||
document.getElementById('statusId').value = id;
|
||||
document.getElementById('statusSel').value = status;
|
||||
document.getElementById('modalStatus').classList.add('open');
|
||||
}
|
||||
function closeModalStatus(){ document.getElementById('modalStatus').classList.remove('open'); }
|
||||
document.querySelectorAll('.modal-bg').forEach(m => m.addEventListener('click', function(e){ if(e.target===this) this.classList.remove('open'); }));
|
||||
<?php if ($msg): ?>
|
||||
(function(){ var t=document.createElement('div'); t.className='toast success'; t.textContent='✅ <?= addslashes($msg) ?>'; document.getElementById('toastCont').appendChild(t); setTimeout(()=>t.remove(),3500); })();
|
||||
<?php endif; if ($err): ?>
|
||||
(function(){ var t=document.createElement('div'); t.className='toast error'; t.textContent='⚠️ <?= addslashes($err) ?>'; document.getElementById('toastCont').appendChild(t); setTimeout(()=>t.remove(),4000); })();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
require_once 'koneksi.php';
|
||||
requireLogin();
|
||||
requireRole('admin','pengurus');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Riwayat Pendidikan & Pelatihan — WebGIS Kemiskinan</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#080c11;--surface:#0f1620;--surface2:#161f2c;--border:#243044;--border2:#2f3f57;
|
||||
--text:#e8edf3;--muted:#6b7e96;
|
||||
--red:#e85050;--green:#27ae72;--yellow:#f0b429;--blue:#4a9eff;
|
||||
--font-head:'Plus Jakarta Sans',sans-serif;--font-body:'Inter',sans-serif;
|
||||
}
|
||||
body{font-family:var(--font-body);background:var(--bg);color:var(--text);}
|
||||
.topnav{display:flex;align-items:center;gap:14px;padding:14px 24px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100;}
|
||||
.nav-logo{font-family:var(--font-head);font-weight:800;font-size:15px;}
|
||||
.nav-links{display:flex;gap:6px;flex:1;}
|
||||
.nav-link{padding:7px 14px;border-radius:8px;color:var(--muted);font-size:13px;text-decoration:none;transition:background .15s,color .15s;}
|
||||
.nav-link:hover,.nav-link.active{background:rgba(232,80,80,.12);color:var(--red);}
|
||||
.container{max-width:1200px;margin:0 auto;padding:28px 24px;}
|
||||
h2{font-family:var(--font-head);font-size:22px;font-weight:800;margin-bottom:6px;}
|
||||
.subtitle{color:var(--muted);font-size:14px;margin-bottom:24px;}
|
||||
|
||||
.alert{background:rgba(240,180,41,.08);border:1px solid rgba(240,180,41,.25);border-radius:12px;padding:14px 18px;color:var(--yellow);font-size:13px;margin-bottom:24px;display:flex;align-items:center;gap:10px;}
|
||||
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start;}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;}
|
||||
.card-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
|
||||
.card-title{font-family:var(--font-head);font-size:14px;font-weight:700;}
|
||||
.card-body{padding:18px 20px;}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
th{text-align:left;padding:8px 12px;background:var(--surface2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid var(--border);}
|
||||
td{padding:8px 12px;border-bottom:1px solid var(--border2);}
|
||||
tr:hover td{background:rgba(255,255,255,.02);}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;}
|
||||
.b-lulus{background:rgba(39,174,114,.2);color:var(--green);}
|
||||
.b-berjalan{background:rgba(74,158,255,.2);color:var(--blue);}
|
||||
.b-putus{background:rgba(232,80,80,.2);color:var(--red);}
|
||||
.b-formal{background:rgba(74,158,255,.12);color:var(--blue);}
|
||||
.b-pelatihan{background:rgba(155,89,182,.12);color:#9b59b6;}
|
||||
|
||||
.btn{padding:8px 16px;border:none;border-radius:9px;font-family:var(--font-body);font-size:12.5px;font-weight:600;cursor:pointer;transition:opacity .2s;}
|
||||
.btn:hover{opacity:.85;}
|
||||
.btn-primary{background:var(--red);color:#fff;}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text);}
|
||||
.btn-sm{padding:5px 10px;font-size:11px;border-radius:7px;}
|
||||
|
||||
.form-group{margin-bottom:14px;}
|
||||
.form-group label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px;font-weight:500;}
|
||||
.form-group input,.form-group select,.form-group textarea{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:9px 12px;color:var(--text);font-family:var(--font-body);font-size:13px;outline:none;transition:border-color .2s;}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--red);}
|
||||
.form-group select option{background:var(--surface2);}
|
||||
|
||||
.warn-box{background:rgba(240,180,41,.08);border:1px solid rgba(240,180,41,.25);border-radius:8px;padding:10px 12px;color:var(--yellow);font-size:12px;margin-top:8px;display:none;}
|
||||
|
||||
.toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;}
|
||||
.toast{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 16px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideIn .2s ease;min-width:260px;}
|
||||
.toast.success{border-left:3px solid var(--green);}
|
||||
.toast.error{border-left:3px solid var(--red);}
|
||||
@keyframes slideIn{from{transform:translateX(20px);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<nav class="topnav">
|
||||
<div class="nav-logo">🗺️ WebGIS Kemiskinan</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php" class="nav-link">Peta</a>
|
||||
<a href="dashboard.php" class="nav-link">Dashboard</a>
|
||||
<a href="keluarga.php" class="nav-link">Keluarga</a>
|
||||
<a href="riwayat_pendidikan.php" class="nav-link active">Pendidikan</a>
|
||||
</div>
|
||||
<a href="logout.php" style="color:var(--red);font-size:12px;text-decoration:none">Logout</a>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h2>🎓 Riwayat Pendidikan & Pelatihan</h2>
|
||||
<p class="subtitle">Kelola data pendidikan formal dan pelatihan kerja penduduk, lengkap dengan validasi logika usia.</p>
|
||||
|
||||
<div class="alert">
|
||||
⚠️ <strong>Validasi Aktif:</strong> Sistem otomatis memvalidasi kesesuaian usia dengan jenjang pendidikan. Usia 7–12 tahun wajib SD, 13–15 tahun wajib SMP, 16–18 tahun wajib SMA/SMK.
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
<!-- FORM TAMBAH -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">➕ Tambah Riwayat Pendidikan / Pelatihan</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="formPddk" onsubmit="submitPddk(event)">
|
||||
<input type="hidden" id="p_id" name="id">
|
||||
<div class="form-group">
|
||||
<label>Cari Penduduk *</label>
|
||||
<input type="text" id="cariPenduduk" placeholder="Ketik nama atau NIK..." oninput="cariPendudukFn(this.value)">
|
||||
<div id="hasilCari" style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;margin-top:4px;display:none;max-height:160px;overflow-y:auto;font-size:13px;"></div>
|
||||
<input type="hidden" name="penduduk_id" id="p_penduduk_id">
|
||||
<div id="selectedPenduduk" style="font-size:12px;color:var(--blue);margin-top:5px;"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis *</label>
|
||||
<select name="jenis" id="p_jenis" required>
|
||||
<option value="formal">Pendidikan Formal</option>
|
||||
<option value="pelatihan">Pelatihan Kerja</option>
|
||||
<option value="kursus">Kursus / Non-formal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Institusi</label>
|
||||
<input type="text" name="nama_institusi" placeholder="SDN 1 Pontianak / BLK Pontianak">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenjang / Nama Program</label>
|
||||
<input type="text" name="jenjang" id="p_jenjang" placeholder="SD / SMP / Menjahit / Las">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jurusan (jika ada)</label>
|
||||
<input type="text" name="jurusan" placeholder="IPA / Teknik / dll">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group">
|
||||
<label>Tahun Masuk</label>
|
||||
<input type="number" name="tahun_masuk" min="1950" max="2030" placeholder="2015">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tahun Lulus</label>
|
||||
<input type="number" name="tahun_lulus" min="1950" max="2030" placeholder="2021 (kosong jika berjalan)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select name="status" required>
|
||||
<option value="lulus">Lulus</option>
|
||||
<option value="sedang_berjalan">Sedang Berjalan</option>
|
||||
<option value="putus">Putus / Tidak Selesai</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="warn-box" id="warnPddk"></div>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;margin-top:4px">💾 Simpan</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABEL RIWAYAT -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📋 Semua Riwayat Pendidikan</div>
|
||||
<input type="text" placeholder="Cari nama..." style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:6px 12px;color:var(--text);font-size:12px;outline:none;width:150px" oninput="loadRiwayat(this.value)">
|
||||
</div>
|
||||
<div style="overflow-x:auto;max-height:620px;overflow-y:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Penduduk</th><th>Jenis</th><th>Institusi</th><th>Jenjang</th><th>Thn</th><th>Status</th><th>Aksi</th></tr>
|
||||
</thead>
|
||||
<tbody id="riwayatBody">
|
||||
<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:24px">Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WARGA POTENSI BUTUH PELATIHAN -->
|
||||
<div class="card" style="margin-top:18px">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🎯 Deteksi Potensi Butuh Intervensi Pendidikan</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Nama</th><th>Umur</th><th>Status Ekonomi</th><th>Pendidikan Terakhir</th><th>Status Pendidikan</th><th>Pekerjaan</th><th>Rekomendasi</th></tr>
|
||||
</thead>
|
||||
<tbody id="intervensiBody">
|
||||
<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:24px">Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---- SEARCH PENDUDUK ----
|
||||
let cariTimer;
|
||||
function cariPendudukFn(val) {
|
||||
clearTimeout(cariTimer);
|
||||
if (val.length < 2) { document.getElementById('hasilCari').style.display = 'none'; return; }
|
||||
cariTimer = setTimeout(() => {
|
||||
fetch(`api/penduduk.php?action=list&search=${encodeURIComponent(val)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const box = document.getElementById('hasilCari');
|
||||
if (!data.success || !data.data.length) { box.style.display = 'none'; return; }
|
||||
box.style.display = '';
|
||||
box.innerHTML = data.data.slice(0, 8).map(p =>
|
||||
`<div onclick="pilihPenduduk(${p.id},'${p.nama}',${p.umur || 0})" style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--border);transition:background .1s" onmouseover="this.style.background='var(--surface3)'" onmouseout="this.style.background=''">${p.nama} <span style="color:var(--muted);font-size:11px">— ${p.nik} — ${p.umur || '?'}th</span></div>`
|
||||
).join('');
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function pilihPenduduk(id, nama, umur) {
|
||||
document.getElementById('p_penduduk_id').value = id;
|
||||
document.getElementById('cariPenduduk').value = nama;
|
||||
document.getElementById('selectedPenduduk').textContent = `✓ Dipilih: ${nama} (${umur} tahun)`;
|
||||
document.getElementById('hasilCari').style.display = 'none';
|
||||
cekWarnPddk(umur);
|
||||
}
|
||||
|
||||
function cekWarnPddk(umur) {
|
||||
const jenjang = document.getElementById('p_jenjang').value.toLowerCase();
|
||||
const warn = document.getElementById('warnPddk');
|
||||
let msg = '';
|
||||
if (umur >= 7 && umur <= 12 && !['sd'].some(j => jenjang.includes(j))) {
|
||||
msg = `⚠️ Usia ${umur} tahun seharusnya masih SD.`;
|
||||
} else if (umur >= 13 && umur <= 15 && !['smp','sd'].some(j => jenjang.includes(j))) {
|
||||
msg = `⚠️ Usia ${umur} tahun seharusnya SMP.`;
|
||||
} else if (umur >= 16 && umur <= 18 && !['sma','smk','smp','sd'].some(j => jenjang.includes(j))) {
|
||||
msg = `⚠️ Usia ${umur} tahun seharusnya SMA/SMK.`;
|
||||
}
|
||||
warn.textContent = msg;
|
||||
warn.style.display = msg ? '' : 'none';
|
||||
}
|
||||
|
||||
function submitPddk(e) {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
if (!fd.get('penduduk_id')) { toast('❌ Pilih penduduk terlebih dahulu', 'error'); return; }
|
||||
fetch('api/riwayat_pendidikan.php?action=save', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
toast('✅ Riwayat pendidikan disimpan', 'success');
|
||||
e.target.reset();
|
||||
document.getElementById('selectedPenduduk').textContent = '';
|
||||
document.getElementById('p_penduduk_id').value = '';
|
||||
loadRiwayat();
|
||||
} else {
|
||||
toast('❌ ' + data.error, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- LOAD RIWAYAT ----
|
||||
function loadRiwayat(search = '') {
|
||||
fetch(`api/riwayat_pendidikan.php?action=list&search=${encodeURIComponent(search)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('riwayatBody');
|
||||
if (!data.success || !data.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:24px">Tidak ada data</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = data.data.map(r => `
|
||||
<tr>
|
||||
<td><strong>${r.nama_penduduk}</strong><div style="font-size:10px;color:var(--muted)">${r.nik}</div></td>
|
||||
<td><span class="badge b-${r.jenis}">${r.jenis}</span></td>
|
||||
<td style="font-size:12px">${r.nama_institusi || '-'}</td>
|
||||
<td>${r.jenjang || '-'}</td>
|
||||
<td style="font-size:11px">${r.tahun_masuk || '-'} – ${r.tahun_lulus || '…'}</td>
|
||||
<td><span class="badge b-${r.status}">${r.status.replace('_',' ')}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-outline btn-sm" onclick="hapusRiwayat(${r.id})" style="color:var(--red)">🗑</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
function hapusRiwayat(id) {
|
||||
if (!confirm('Hapus riwayat ini?')) return;
|
||||
fetch('api/riwayat_pendidikan.php?action=delete', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `id=${id}`
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) { toast('🗑 Dihapus', 'success'); loadRiwayat(); }
|
||||
else toast('❌ ' + data.error, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- DETEKSI INTERVENSI ----
|
||||
function loadIntervensi() {
|
||||
fetch('api/penduduk.php?action=list')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('intervensiBody');
|
||||
if (!data.success) return;
|
||||
const perlu = data.data.filter(p => {
|
||||
const u = p.umur;
|
||||
if (u >= 7 && u <= 15 && p.status_pendidikan !== 'masih_sekolah') return true;
|
||||
if (u >= 16 && u <= 25 && p.status_ekonomi === 'miskin' && p.status_pendidikan === 'tidak_sekolah') return true;
|
||||
return false;
|
||||
}).slice(0, 30);
|
||||
|
||||
if (!perlu.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--green);padding:24px">✅ Tidak ada warga yang terdeteksi butuh intervensi pendidikan mendesak.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = perlu.map(p => {
|
||||
let rek = '';
|
||||
if (p.umur >= 7 && p.umur <= 12) rek = '📚 Daftarkan ke SD terdekat';
|
||||
else if (p.umur >= 13 && p.umur <= 15) rek = '📚 Daftarkan ke SMP';
|
||||
else if (p.umur >= 16 && p.umur <= 18) rek = '📚 Daftarkan ke SMA/SMK';
|
||||
else rek = '🔧 Ikutkan pelatihan kerja BLK';
|
||||
return `<tr>
|
||||
<td><strong>${p.nama}</strong></td>
|
||||
<td>${p.umur} tahun</td>
|
||||
<td><span class="badge" style="background:rgba(232,80,80,.2);color:var(--red)">${p.status_ekonomi}</span></td>
|
||||
<td>${p.pendidikan_terakhir.replace('_',' ').toUpperCase()}</td>
|
||||
<td style="color:var(--red)">${p.status_pendidikan.replace('_',' ')}</td>
|
||||
<td>${p.pekerjaan || '-'}</td>
|
||||
<td style="color:var(--yellow);font-size:12px">${rek}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
});
|
||||
}
|
||||
|
||||
function toast(msg, type='info') {
|
||||
const c = document.getElementById('toastContainer');
|
||||
const t = document.createElement('div');
|
||||
t.className = `toast ${type}`;
|
||||
t.textContent = msg;
|
||||
c.appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
|
||||
loadRiwayat();
|
||||
loadIntervensi();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
php_flag engine off
|
||||
<FilesMatch "\.(php|phtml|pl|py|cgi)$">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
+1128
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,295 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manual Book WebGIS Project</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #07101f;
|
||||
--panel: #0d1a2e;
|
||||
--panel-soft: #10213a;
|
||||
--line: rgba(116, 213, 255, .18);
|
||||
--text: #eef7ff;
|
||||
--muted: #a8bbcc;
|
||||
--cyan: #38e8ff;
|
||||
--mint: #53f2b7;
|
||||
--amber: #ffbd59;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 12% 0%, rgba(56,232,255,.24), transparent 32%),
|
||||
radial-gradient(circle at 88% 12%, rgba(83,242,183,.16), transparent 30%),
|
||||
linear-gradient(145deg, #050914, var(--bg));
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1080px, calc(100% - 36px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 44px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.brand strong {
|
||||
font-size: 18px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.brand span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
padding: 0 15px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: rgba(255,255,255,.06);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 30px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(56,232,255,.13), transparent 40%),
|
||||
rgba(13, 26, 46, .84);
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,.36);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--mint);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 800px;
|
||||
margin: 14px 0 0;
|
||||
font-size: clamp(36px, 6vw, 72px);
|
||||
line-height: .95;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 780px;
|
||||
margin: 20px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 26px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(13, 26, 46, .72);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(116, 213, 255, .14);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.card p,
|
||||
.card li {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 9px 0 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: inline-flex;
|
||||
margin-top: 14px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid rgba(56,232,255,.32);
|
||||
border-radius: 10px;
|
||||
color: var(--cyan);
|
||||
background: rgba(56,232,255,.08);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shell {
|
||||
width: min(100% - 22px, 560px);
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.section {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<strong>Manual Book WebGIS Project</strong>
|
||||
<span>Panduan penggunaan folder gabungan 01 sampai 05</span>
|
||||
</div>
|
||||
<a class="back" href="index.html">Kembali ke Landing</a>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="eyebrow">Dokumentasi akses</div>
|
||||
<h1>Panduan cepat menjalankan semua project.</h1>
|
||||
<p class="lead">
|
||||
Folder utama berisi lima project WebGIS. Project 01 sampai 04 dapat dibuka langsung tanpa login, sedangkan project 05 adalah sistem administrasi yang harus masuk melalui halaman login.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Alur Penggunaan</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>1. Buka landing utama</h3>
|
||||
<p>Akses folder melalui XAMPP di <strong>http://localhost/websig/WebGIS_Project/</strong>. Landing menjadi pusat navigasi semua project.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>2. Pilih project sesuai kebutuhan</h3>
|
||||
<p>Gunakan launchpad di landing untuk membuka modul peta titik, digitasi, choropleth, atau dashboard admin.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>3. Gunakan project publik</h3>
|
||||
<p>Project publik langsung menampilkan peta. Beberapa fitur membutuhkan database lokal sesuai file koneksi masing-masing project.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>4. Login untuk project 05</h3>
|
||||
<p>Project 05 berisi modul internal seperti dashboard, laporan, keluarga, pendidikan, dan administrasi lain sehingga aksesnya memakai autentikasi.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Daftar Project</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Project 01 - GIS SPBU Dasar</h3>
|
||||
<p>Pemetaan titik SPBU berbasis Leaflet. Cocok untuk melihat data lokasi dan status marker sederhana.</p>
|
||||
<a class="project-link" href="01/index.php">Buka Project 01</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Project 02 - Digitasi Objek Spasial</h3>
|
||||
<p>Digunakan untuk menggambar dan mengelola objek jalan, parsil, dan kerusakan pada peta.</p>
|
||||
<a class="project-link" href="02/index.html">Buka Project 02</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Project 03 - Choropleth Pontianak</h3>
|
||||
<p>Menampilkan peta tematik Kota Pontianak dengan pewarnaan wilayah berdasarkan nilai atau kategori.</p>
|
||||
<a class="project-link" href="03/Choropleth.html">Buka Project 03</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Project 04 - GIS SPBU Pro</h3>
|
||||
<p>Versi lanjutan project SPBU dengan tampilan popup dan pengalaman penggunaan yang lebih rapi.</p>
|
||||
<a class="project-link" href="04/index.php">Buka Project 04</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Project 05 - Sistem WebGIS UAS</h3>
|
||||
<p>Dashboard administrasi dengan modul data internal. Akses utama diarahkan ke halaman login.</p>
|
||||
<a class="project-link" href="05/webgis_uas_FIXED_v2/webgis_uas_FIXED/login.php">Login Project 05</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Catatan Database</h3>
|
||||
<p class="note">Jika data tidak tampil, pastikan database SQL sudah diimport dan konfigurasi koneksi pada masing-masing folder sudah sesuai XAMPP lokal.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Struktur Folder Setelah Dirapikan</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><strong>01</strong> - GIS SPBU dasar.</li>
|
||||
<li><strong>02</strong> - Digitasi jalan, parsil, dan kerusakan.</li>
|
||||
<li><strong>03</strong> - Choropleth Kota Pontianak.</li>
|
||||
<li><strong>04</strong> - GIS SPBU Pro.</li>
|
||||
<li><strong>05</strong> - Sistem WebGIS UAS dengan login.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user