add files
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
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api.php – REST API untuk WebGIS SPBU Pontianak
|
||||
// Endpoint:
|
||||
// GET api.php → ambil semua SPBU
|
||||
// POST api.php → tambah SPBU baru
|
||||
// PUT api.php?id=N → update SPBU
|
||||
// DELETE api.php?id=N → hapus SPBU
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── Ambil semua SPBU ─────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
$res = $conn->query(
|
||||
"SELECT id, nama_spbu, nomor_wa, status_24jam,
|
||||
latitude, longitude, created_at
|
||||
FROM spbu ORDER BY created_at DESC"
|
||||
);
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['latitude'] = (float) $row['latitude'];
|
||||
$row['longitude'] = (float) $row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Tambah SPBU baru ─────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssdd', $nama, $wa, $status, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'SPBU berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Update SPBU ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE spbu SET nama_spbu=?, nomor_wa=?, status_24jam=?,
|
||||
latitude=?, longitude=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssddi', $nama, $wa, $status, $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ───────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config.php – Konfigurasi Koneksi Database
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_spbu');
|
||||
|
||||
function getConnection(): mysqli {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]));
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- WebGIS SPBU Pontianak
|
||||
-- Database Structure
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(150) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
status_24jam ENUM('yes','no') NOT NULL DEFAULT 'no',
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data – beberapa SPBU di Pontianak
|
||||
-- ============================================================
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Jl. Ahmad Yani', '6281234567890', 'yes', -0.0262619, 109.3425923),
|
||||
('SPBU Shell Jl. Tanjungpura', '6281298765432', 'no', -0.0318750, 109.3319870),
|
||||
('SPBU Pertamina Jl. Gajahmada', '6281211223344', 'yes', -0.0393160, 109.3266190),
|
||||
('SPBU Pertamina Jl. Sultan Hamid II', '6281355667788', 'no', -0.0159870, 109.3571200);
|
||||
+976
@@ -0,0 +1,976 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>WebGIS SPBU — Kota Pontianak</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
||||
|
||||
<style>
|
||||
/* ══════════════════════════════════════
|
||||
RESET & VARIABLES
|
||||
══════════════════════════════════════ */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface2: #21262d;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--muted: #8b949e;
|
||||
--accent: #f97316;
|
||||
--accent-dk: #c2591a;
|
||||
--green: #3fb950;
|
||||
--red: #f85149;
|
||||
--radius: 12px;
|
||||
--shadow: 0 8px 32px rgba(0,0,0,.55);
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════
|
||||
LAYOUT
|
||||
══════════════════════════════════════ */
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
grid-template-rows: 60px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────── */
|
||||
header {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 0 24px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 10;
|
||||
}
|
||||
.logo-icon {
|
||||
width: 34px; height: 34px;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 15px; font-weight: 700;
|
||||
letter-spacing: -.2px;
|
||||
}
|
||||
header h1 span { color: var(--accent); }
|
||||
.header-meta {
|
||||
margin-left: auto;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: var(--surface2);
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.dot-live {
|
||||
display: inline-block;
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
margin-right: 6px;
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity: 1; }
|
||||
50% { opacity: .35; }
|
||||
}
|
||||
|
||||
/* ── Sidebar ─────────────────────────── */
|
||||
aside {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-header h2 {
|
||||
font-size: 13px; font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .8px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.count-badge {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 26px; font-weight: 700;
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
}
|
||||
.count-badge small { font-size: 12px; color: var(--muted); font-family: inherit; }
|
||||
|
||||
/* stats row */
|
||||
.stats-row {
|
||||
display: flex; gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.stat-pill {
|
||||
flex: 1; text-align: center;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 4px;
|
||||
font-size: 11px; color: var(--muted);
|
||||
}
|
||||
.stat-pill strong { display: block; font-size: 18px; font-weight: 700; margin-bottom: 2px; }
|
||||
.stat-pill.green strong { color: var(--green); }
|
||||
.stat-pill.red strong { color: var(--red); }
|
||||
|
||||
/* search */
|
||||
.search-wrap {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.search-wrap input {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px 8px 34px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px center;
|
||||
}
|
||||
.search-wrap input:focus { border-color: var(--accent); }
|
||||
|
||||
/* list */
|
||||
#spbu-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
#spbu-list::-webkit-scrollbar { width: 4px; }
|
||||
#spbu-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
|
||||
.spbu-card {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 13px 15px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color .2s, transform .15s;
|
||||
position: relative;
|
||||
}
|
||||
.spbu-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
.spbu-card.active { border-color: var(--accent); background: #1d2029; }
|
||||
|
||||
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
||||
.card-name { font-size: 13px; font-weight: 600; line-height: 1.35; }
|
||||
.badge {
|
||||
font-size: 10px; font-weight: 700; padding: 2px 8px;
|
||||
border-radius: 20px; white-space: nowrap; flex-shrink: 0;
|
||||
letter-spacing: .4px; text-transform: uppercase;
|
||||
}
|
||||
.badge.yes { background: rgba(63,185,80,.18); color: var(--green); border: 1px solid var(--green); }
|
||||
.badge.no { background: rgba(248,81,73,.15); color: var(--red); border: 1px solid var(--red); }
|
||||
|
||||
.card-wa {
|
||||
font-size: 11px; color: var(--muted); margin-top: 5px;
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.card-wa svg { flex-shrink: 0; }
|
||||
|
||||
.card-actions {
|
||||
display: flex; gap: 6px; margin-top: 10px;
|
||||
}
|
||||
.btn-sm {
|
||||
flex: 1; padding: 5px 0;
|
||||
font-size: 11px; font-weight: 600;
|
||||
border-radius: 6px; cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--muted);
|
||||
font-family: inherit;
|
||||
transition: all .18s;
|
||||
}
|
||||
.btn-sm:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn-sm.danger:hover { background: var(--red); border-color: var(--red); }
|
||||
.btn-sm.move:hover { background: #f97316; border-color: #f97316; color: #fff; }
|
||||
|
||||
/* add-new CTA */
|
||||
.cta-box {
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.btn-add {
|
||||
width: 100%;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 11px;
|
||||
font-size: 13px; font-weight: 700;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background .2s, transform .15s;
|
||||
display: flex; align-items: center; justify-content: center; gap: 7px;
|
||||
}
|
||||
.btn-add:hover { background: var(--accent-dk); transform: translateY(-1px); }
|
||||
|
||||
/* ── Map ─────────────────────────────── */
|
||||
#map {
|
||||
width: 100%; height: 100%;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════
|
||||
MODAL
|
||||
══════════════════════════════════════ */
|
||||
.overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.7);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .25s;
|
||||
}
|
||||
.overlay.open { opacity: 1; pointer-events: all; }
|
||||
|
||||
.modal {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 28px 28px 24px;
|
||||
width: 420px; max-width: 95vw;
|
||||
box-shadow: var(--shadow);
|
||||
transform: translateY(12px);
|
||||
transition: transform .25s;
|
||||
}
|
||||
.overlay.open .modal { transform: translateY(0); }
|
||||
|
||||
.modal h3 {
|
||||
font-size: 16px; font-weight: 700; margin-bottom: 20px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.modal h3 .icon {
|
||||
width: 32px; height: 32px;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
display: grid; place-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label {
|
||||
display: block; font-size: 12px; font-weight: 600;
|
||||
color: var(--muted); margin-bottom: 6px;
|
||||
text-transform: uppercase; letter-spacing: .5px;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s;
|
||||
appearance: none;
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group select:focus { border-color: var(--accent); }
|
||||
|
||||
.coord-row {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex; gap: 10px; margin-top: 20px;
|
||||
}
|
||||
.btn-cancel {
|
||||
flex: 1; padding: 11px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--muted);
|
||||
font-size: 13px; font-weight: 600;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-cancel:hover { border-color: var(--red); color: var(--red); }
|
||||
.btn-save {
|
||||
flex: 2; padding: 11px;
|
||||
background: var(--accent);
|
||||
border: none; border-radius: var(--radius);
|
||||
color: #fff;
|
||||
font-size: 13px; font-weight: 700;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: background .2s;
|
||||
}
|
||||
.btn-save:hover { background: var(--accent-dk); }
|
||||
|
||||
/* coord hint */
|
||||
.coord-hint {
|
||||
font-size: 11px; color: var(--muted);
|
||||
background: var(--surface2);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px; margin-bottom: 16px;
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
}
|
||||
|
||||
/* ── Floating bar pindah titik ───────── */
|
||||
#move-bar {
|
||||
position: absolute;
|
||||
bottom: 32px; left: 50%; transform: translateX(-50%) translateY(80px);
|
||||
z-index: 900;
|
||||
background: var(--surface);
|
||||
border: 1.5px solid var(--accent);
|
||||
border-radius: 40px;
|
||||
padding: 10px 16px 10px 20px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0,0,0,.6), 0 0 0 4px rgba(249,115,22,.12);
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: transform .3s cubic-bezier(.34,1.56,.64,1), opacity .25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#move-bar.active {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1; pointer-events: all;
|
||||
}
|
||||
.move-bar-label {
|
||||
font-size: 12px; font-weight: 600; color: var(--accent);
|
||||
display: flex; align-items: center; gap: 7px;
|
||||
}
|
||||
.move-bar-label .pulse-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: pulse 1.2s infinite;
|
||||
}
|
||||
.btn-confirm {
|
||||
padding: 6px 16px;
|
||||
background: var(--green); border: none;
|
||||
border-radius: 20px; color: #fff;
|
||||
font-size: 12px; font-weight: 700;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.btn-confirm:hover { opacity: .85; }
|
||||
.btn-cancel-move {
|
||||
padding: 6px 14px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px; color: var(--muted);
|
||||
font-size: 12px; font-weight: 600;
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
.btn-cancel-move:hover { border-color: var(--red); color: var(--red); }
|
||||
|
||||
/* ── Toast ───────────────────────────── */
|
||||
#toast {
|
||||
position: fixed; bottom: 24px; right: 24px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 18px;
|
||||
font-size: 13px; font-weight: 500;
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 9999;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
}
|
||||
#toast.show { transform: translateY(0); opacity: 1; }
|
||||
#toast.success { border-color: var(--green); }
|
||||
#toast.error { border-color: var(--red); }
|
||||
|
||||
/* ── Leaflet popup custom ────────────── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--surface) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--surface) !important; }
|
||||
.leaflet-popup-content { margin: 14px 16px !important; }
|
||||
.popup-title { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
|
||||
.popup-wa { font-size: 12px; color: var(--muted); }
|
||||
.popup-status { margin-top: 8px; }
|
||||
|
||||
/* empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.empty-state .emoji { font-size: 36px; margin-bottom: 10px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app { grid-template-columns: 1fr; grid-template-rows: 60px 1fr 260px; }
|
||||
aside { border-right: none; border-bottom: 1px solid var(--border); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app">
|
||||
|
||||
<!-- ═══════════ HEADER ═══════════ -->
|
||||
<header>
|
||||
<div class="logo-icon">⛽</div>
|
||||
<h1>WebGIS <span>SPBU</span> Pontianak</h1>
|
||||
<div class="header-meta">
|
||||
<span class="dot-live"></span>Live Map
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════ SIDEBAR ═══════════ -->
|
||||
<aside>
|
||||
<div class="sidebar-header">
|
||||
<h2>Total SPBU</h2>
|
||||
<div class="count-badge" id="total-count">0 <small>titik</small></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-pill green">
|
||||
<strong id="stat-24">0</strong>
|
||||
24 Jam
|
||||
</div>
|
||||
<div class="stat-pill red">
|
||||
<strong id="stat-non24">0</strong>
|
||||
Non-24 Jam
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-wrap">
|
||||
<input type="text" id="search-input" placeholder="Cari nama SPBU…" oninput="filterList()"/>
|
||||
</div>
|
||||
|
||||
<div id="spbu-list">
|
||||
<div class="empty-state">
|
||||
<div class="emoji">🗺️</div>
|
||||
Memuat data…
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-box">
|
||||
<button class="btn-add" onclick="openAddMode()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Klik Peta untuk Tambah SPBU
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ═══════════ MAP ═══════════ -->
|
||||
<div id="map" style="position:relative;">
|
||||
<!-- Floating bar konfirmasi pindah titik -->
|
||||
<div id="move-bar">
|
||||
<div class="move-bar-label">
|
||||
<span class="pulse-dot"></span>
|
||||
Geser pin ke lokasi baru
|
||||
</div>
|
||||
<button class="btn-confirm" onclick="confirmMove()">✅ Konfirmasi</button>
|
||||
<button class="btn-cancel-move" onclick="cancelMove()">✕ Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ MODAL FORM ═══════════ -->
|
||||
<div class="overlay" id="modal-overlay">
|
||||
<div class="modal">
|
||||
<h3>
|
||||
<span class="icon">⛽</span>
|
||||
<span id="modal-title">Tambah SPBU</span>
|
||||
</h3>
|
||||
|
||||
<div class="coord-hint" id="coord-hint">
|
||||
📍 Koordinat diambil dari titik yang Anda klik di peta
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pertamina Jl. Ahmad Yani"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nomor WhatsApp</label>
|
||||
<input type="text" id="f-wa" placeholder="Contoh: 6281234567890"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status 24 Jam</label>
|
||||
<select id="f-status">
|
||||
<option value="yes">✅ Ya – Buka 24 Jam</option>
|
||||
<option value="no">❌ Tidak – Jam Terbatas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="coord-row">
|
||||
<div class="form-group">
|
||||
<label>Latitude</label>
|
||||
<input type="number" id="f-lat" step="0.000001" placeholder="-0.0262619"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Longitude</label>
|
||||
<input type="number" id="f-lng" step="0.000001" placeholder="109.3425923"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
<button class="btn-save" onclick="saveSpbu()">💾 Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let spbuData = [];
|
||||
let markers = {}; // id → marker
|
||||
let editId = null;
|
||||
let addMode = false;
|
||||
let tempMarker = null;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MAP INIT — center: Pontianak
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
zoomControl: false
|
||||
});
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
|
||||
// Tile layer OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── Custom icons ──────────────────────────────────────────
|
||||
function makeIcon(is24) {
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:36px;height:36px;
|
||||
background:${is24 ? '#3fb950' : '#f97316'};
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 3px 10px rgba(0,0,0,.5);
|
||||
"></div>`,
|
||||
iconSize: [36, 36],
|
||||
iconAnchor: [18, 36],
|
||||
popupAnchor: [0, -38]
|
||||
});
|
||||
}
|
||||
|
||||
const tempIcon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:34px;height:34px;
|
||||
background:#f97316;
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 3px 14px rgba(249,115,22,.7);
|
||||
animation: blink .7s infinite alternate;
|
||||
"></div>
|
||||
<style>@keyframes blink{from{opacity:1}to{opacity:.4}}</style>`,
|
||||
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -36]
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// API CALLS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const API = 'api.php';
|
||||
|
||||
async function fetchAll() {
|
||||
try {
|
||||
const r = await fetch(API);
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
spbuData = j.data;
|
||||
renderList();
|
||||
renderMarkers();
|
||||
updateStats();
|
||||
}
|
||||
} catch(e) { showToast('Gagal memuat data. Pastikan server berjalan.', 'error'); }
|
||||
}
|
||||
|
||||
async function saveSpbu() {
|
||||
const nama = document.getElementById('f-nama').value.trim();
|
||||
const wa = document.getElementById('f-wa').value.trim();
|
||||
const status = document.getElementById('f-status').value;
|
||||
const lat = parseFloat(document.getElementById('f-lat').value);
|
||||
const lng = parseFloat(document.getElementById('f-lng').value);
|
||||
|
||||
if (!nama || !wa || isNaN(lat) || isNaN(lng)) {
|
||||
showToast('Harap isi semua field dengan benar!', 'error'); return;
|
||||
}
|
||||
|
||||
const payload = { nama_spbu: nama, nomor_wa: wa, status_24jam: status, latitude: lat, longitude: lng };
|
||||
|
||||
try {
|
||||
const r = await fetch(API + (editId ? `?id=${editId}` : ''), {
|
||||
method : editId ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify(payload)
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast(editId ? 'SPBU berhasil diperbarui! ✏️' : 'SPBU berhasil ditambahkan! ⛽', 'success');
|
||||
closeModal();
|
||||
fetchAll();
|
||||
} else {
|
||||
showToast('Gagal menyimpan: ' + j.message, 'error');
|
||||
}
|
||||
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
|
||||
}
|
||||
|
||||
async function deleteSpbu(id) {
|
||||
if (!confirm('Yakin hapus SPBU ini?')) return;
|
||||
try {
|
||||
const r = await fetch(`${API}?id=${id}`, { method: 'DELETE' });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast('SPBU berhasil dihapus! 🗑️', 'success');
|
||||
fetchAll();
|
||||
}
|
||||
} catch(e) { showToast('Gagal menghapus.', 'error'); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// RENDER
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function renderMarkers() {
|
||||
// clear old
|
||||
Object.values(markers).forEach(m => map.removeLayer(m));
|
||||
markers = {};
|
||||
|
||||
spbuData.forEach(s => {
|
||||
const m = L.marker([s.latitude, s.longitude], { icon: makeIcon(s.status_24jam === 'yes') })
|
||||
.addTo(map)
|
||||
.bindPopup(popupHTML(s));
|
||||
m.on('click', () => highlightCard(s.id));
|
||||
markers[s.id] = m;
|
||||
});
|
||||
}
|
||||
|
||||
function popupHTML(s) {
|
||||
return `
|
||||
<div class="popup-title">⛽ ${escHtml(s.nama_spbu)}</div>
|
||||
<div class="popup-wa">
|
||||
📱 ${escHtml(s.nomor_wa)}
|
||||
</div>
|
||||
<div class="popup-status">
|
||||
<span class="badge ${s.status_24jam}">
|
||||
${s.status_24jam === 'yes' ? '✅ Buka 24 Jam' : '❌ Jam Terbatas'}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
|
||||
<button onclick="openEdit(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #30363d;background:#21262d;color:#e6edf3;font-family:inherit">✏️ Edit</button>
|
||||
<button onclick="startMovePin(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f97316;background:rgba(249,115,22,.1);color:#f97316;font-family:inherit">📌 Pindahkan</button>
|
||||
<button onclick="deleteSpbu(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f85149;background:rgba(248,81,73,.1);color:#f85149;font-family:inherit">🗑️ Hapus</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderList(filter = '') {
|
||||
const list = document.getElementById('spbu-list');
|
||||
const q = filter.toLowerCase();
|
||||
const filtered = spbuData.filter(s => s.nama_spbu.toLowerCase().includes(q));
|
||||
|
||||
if (!filtered.length) {
|
||||
list.innerHTML = `<div class="empty-state"><div class="emoji">${filter ? '🔍' : '📍'}</div>${filter ? 'Tidak ditemukan' : 'Belum ada data SPBU'}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = filtered.map(s => `
|
||||
<div class="spbu-card" id="card-${s.id}" onclick="flyTo(${s.id})">
|
||||
<div class="card-top">
|
||||
<div class="card-name">${escHtml(s.nama_spbu)}</div>
|
||||
<span class="badge ${s.status_24jam}">${s.status_24jam === 'yes' ? '24 Jam' : 'Non-24'}</span>
|
||||
</div>
|
||||
<div class="card-wa">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8 19.79 19.79 0 01.01 1.18C0 .1.68-.15 1.91.08A2 2 0 013.9 2.05v3a2 2 0 01-1.45 1.93 16 16 0 006.39 6.39A2 2 0 0110.77 12h.1l.07.06"/></svg>
|
||||
${escHtml(s.nomor_wa)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn-sm" onclick="event.stopPropagation();openEdit(${s.id})">✏️ Edit</button>
|
||||
<button class="btn-sm move" onclick="event.stopPropagation();startMovePin(${s.id})">📌 Pindahkan</button>
|
||||
<button class="btn-sm danger" onclick="event.stopPropagation();deleteSpbu(${s.id})">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = spbuData.length;
|
||||
const yes = spbuData.filter(s => s.status_24jam === 'yes').length;
|
||||
document.getElementById('total-count').innerHTML = `${total} <small>titik</small>`;
|
||||
document.getElementById('stat-24').textContent = yes;
|
||||
document.getElementById('stat-non24').textContent = total - yes;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MAP INTERACTIONS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let addModeActive = false;
|
||||
|
||||
function openAddMode() {
|
||||
addModeActive = true;
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
showToast('🗺️ Klik pada peta untuk menentukan lokasi SPBU', 'success');
|
||||
}
|
||||
|
||||
map.on('click', function(e) {
|
||||
if (!addModeActive) return;
|
||||
addModeActive = false;
|
||||
map.getContainer().style.cursor = '';
|
||||
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: tempIcon }).addTo(map);
|
||||
|
||||
openModal(null, e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
|
||||
function flyTo(id) {
|
||||
const s = spbuData.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
map.flyTo([s.latitude, s.longitude], 16, { duration: 1.2 });
|
||||
setTimeout(() => {
|
||||
if (markers[id]) markers[id].openPopup();
|
||||
}, 1300);
|
||||
highlightCard(id);
|
||||
}
|
||||
|
||||
function highlightCard(id) {
|
||||
document.querySelectorAll('.spbu-card').forEach(c => c.classList.remove('active'));
|
||||
const card = document.getElementById(`card-${id}`);
|
||||
if (card) {
|
||||
card.classList.add('active');
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// MODAL
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function openModal(id = null, lat = '', lng = '') {
|
||||
editId = id;
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
document.getElementById('modal-title').textContent = id ? 'Edit SPBU' : 'Tambah SPBU Baru';
|
||||
|
||||
if (id) {
|
||||
const s = spbuData.find(x => x.id == id);
|
||||
if (!s) { showToast('Data SPBU tidak ditemukan!', 'error'); return; }
|
||||
document.getElementById('f-nama').value = s.nama_spbu;
|
||||
document.getElementById('f-wa').value = s.nomor_wa;
|
||||
document.getElementById('f-status').value = s.status_24jam;
|
||||
document.getElementById('f-lat').value = s.latitude;
|
||||
document.getElementById('f-lng').value = s.longitude;
|
||||
document.getElementById('coord-hint').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('f-nama').value = '';
|
||||
document.getElementById('f-wa').value = '';
|
||||
document.getElementById('f-status').value = 'no';
|
||||
document.getElementById('f-lat').value = lat;
|
||||
document.getElementById('f-lng').value = lng;
|
||||
document.getElementById('coord-hint').style.display = 'flex';
|
||||
}
|
||||
|
||||
overlay.classList.add('open');
|
||||
}
|
||||
|
||||
function openEdit(id) {
|
||||
id = parseInt(id);
|
||||
if (markers[id]) markers[id].closePopup();
|
||||
openModal(id);
|
||||
}
|
||||
|
||||
function closeModal(cancelled = true) {
|
||||
document.getElementById('modal-overlay').classList.remove('open');
|
||||
editId = null;
|
||||
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PINDAHKAN TITIK – drag mode tanpa modal
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let _moveId = null;
|
||||
let _moveOriginal = null;
|
||||
|
||||
function startMovePin(id) {
|
||||
id = parseInt(id);
|
||||
const s = spbuData.find(x => x.id == id);
|
||||
if (!s) return;
|
||||
|
||||
// Tutup popup jika terbuka
|
||||
if (markers[id]) markers[id].closePopup();
|
||||
|
||||
_moveId = id;
|
||||
_moveOriginal = { lat: s.latitude, lng: s.longitude };
|
||||
|
||||
const m = markers[id];
|
||||
m.dragging.enable();
|
||||
|
||||
// Ganti icon jadi warna biru/terang sebagai penanda aktif
|
||||
m.setIcon(L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="
|
||||
width:40px;height:40px;
|
||||
background:#3b82f6;
|
||||
border:3px solid #fff;
|
||||
border-radius:50% 50% 50% 0;
|
||||
transform:rotate(-45deg);
|
||||
box-shadow:0 0 0 6px rgba(59,130,246,.3),0 4px 14px rgba(0,0,0,.5);
|
||||
"></div>`,
|
||||
iconSize: [40,40], iconAnchor: [20,40], popupAnchor: [0,-42]
|
||||
}));
|
||||
|
||||
// Zoom ke marker agar mudah drag
|
||||
map.flyTo([s.latitude, s.longitude], 16, { duration: .8 });
|
||||
|
||||
// Tampilkan floating bar
|
||||
document.getElementById('move-bar').classList.add('active');
|
||||
|
||||
showToast('📌 Geser pin biru ke lokasi baru lalu klik Konfirmasi', 'success');
|
||||
}
|
||||
|
||||
async function confirmMove() {
|
||||
if (!_moveId) return;
|
||||
const m = markers[_moveId];
|
||||
const ll = m.getLatLng();
|
||||
const s = spbuData.find(x => x.id == _moveId);
|
||||
|
||||
try {
|
||||
const r = await fetch(`api.php?id=${_moveId}`, {
|
||||
method : 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify({
|
||||
nama_spbu : s.nama_spbu,
|
||||
nomor_wa : s.nomor_wa,
|
||||
status_24jam: s.status_24jam,
|
||||
latitude : ll.lat,
|
||||
longitude : ll.lng
|
||||
})
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
showToast('📍 Lokasi SPBU berhasil dipindahkan!', 'success');
|
||||
_endMoveMode(false);
|
||||
fetchAll();
|
||||
} else {
|
||||
showToast('Gagal menyimpan: ' + j.message, 'error');
|
||||
}
|
||||
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
|
||||
}
|
||||
|
||||
function cancelMove() {
|
||||
_endMoveMode(true);
|
||||
showToast('Pemindahan dibatalkan', 'error');
|
||||
}
|
||||
|
||||
function _endMoveMode(restore) {
|
||||
if (!_moveId) return;
|
||||
const m = markers[_moveId];
|
||||
m.dragging.disable();
|
||||
|
||||
// Kembalikan posisi & icon asli jika dibatalkan
|
||||
if (restore && _moveOriginal) {
|
||||
m.setLatLng([_moveOriginal.lat, _moveOriginal.lng]);
|
||||
}
|
||||
const s = spbuData.find(x => x.id == _moveId);
|
||||
if (s) m.setIcon(makeIcon(s.status_24jam === 'yes'));
|
||||
|
||||
document.getElementById('move-bar').classList.remove('active');
|
||||
_moveId = null;
|
||||
_moveOriginal = null;
|
||||
}
|
||||
|
||||
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SEARCH
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function filterList() {
|
||||
const q = document.getElementById('search-input').value;
|
||||
renderList(q);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// TOAST
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
let toastTimer;
|
||||
function showToast(msg, type = 'success') {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.className = `show ${type}`;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { t.className = ''; }, 3200);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// UTILS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
function escHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// INIT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Ekspos ke window agar bisa dipanggil dari dalam popup Leaflet
|
||||
window.openEdit = openEdit;
|
||||
window.deleteSpbu = deleteSpbu;
|
||||
window.startMovePin = startMovePin;
|
||||
|
||||
fetchAll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api.php – REST API untuk WebGIS SPBU Pontianak
|
||||
// Endpoint:
|
||||
// GET api.php → ambil semua SPBU
|
||||
// POST api.php → tambah SPBU baru
|
||||
// PUT api.php?id=N → update SPBU
|
||||
// DELETE api.php?id=N → hapus SPBU
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── Ambil semua SPBU ─────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
$res = $conn->query(
|
||||
"SELECT id, nama_spbu, nomor_wa, status_24jam,
|
||||
latitude, longitude, created_at
|
||||
FROM spbu ORDER BY created_at DESC"
|
||||
);
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['latitude'] = (float) $row['latitude'];
|
||||
$row['longitude'] = (float) $row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Tambah SPBU baru ─────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssdd', $nama, $wa, $status, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'SPBU berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Update SPBU ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE spbu SET nama_spbu=?, nomor_wa=?, status_24jam=?,
|
||||
latitude=?, longitude=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssddi', $nama, $wa, $status, $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ───────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_jalan.php – REST API CRUD Data Jalan (Polyline)
|
||||
// GET api_jalan.php → semua data jalan
|
||||
// GET api_jalan.php?id=N → satu data jalan
|
||||
// POST api_jalan.php → tambah jalan
|
||||
// PUT api_jalan.php?id=N → update jalan
|
||||
// DELETE api_jalan.php?id=N → hapus jalan
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── GET ──────────────────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $conn->prepare("SELECT * FROM jalan WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if ($row) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['panjang_meter'] = (float) $row['panjang_meter'];
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
echo json_encode(['status' => 'success', 'data' => $row]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$res = $conn->query("SELECT * FROM jalan ORDER BY created_at DESC");
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['panjang_meter'] = (float) $row['panjang_meter'];
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
}
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── POST (Tambah) ─────────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_jalan'] ?? '');
|
||||
$status = $body['status_jalan'] ?? '';
|
||||
$panjang = (float) ($body['panjang_meter'] ?? 0);
|
||||
$geojson = isset($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: null;
|
||||
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!$nama || !in_array($status, $valid_status) || !$geojson || $panjang <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('ssds', $nama, $status, $panjang, $geojson);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Data jalan berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── PUT (Update) ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_jalan'] ?? '');
|
||||
$status = $body['status_jalan'] ?? '';
|
||||
$panjang = (float) ($body['panjang_meter'] ?? 0);
|
||||
$geojson = isset($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: null;
|
||||
|
||||
$valid_status = ['Nasional', 'Provinsi', 'Kabupaten'];
|
||||
if (!$nama || !in_array($status, $valid_status) || !$geojson || $panjang <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE jalan SET nama_jalan=?, status_jalan=?, panjang_meter=?, geojson=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('ssdsi', $nama, $status, $panjang, $geojson, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data jalan berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── DELETE ────────────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM jalan WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data jalan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_kerusakan.php – REST API untuk Titik Kerusakan Jalan
|
||||
// Endpoint:
|
||||
// GET api_kerusakan.php → ambil semua titik kerusakan
|
||||
// POST api_kerusakan.php → tambah titik baru
|
||||
// PUT api_kerusakan.php?id=N → update titik
|
||||
// DELETE api_kerusakan.php?id=N → hapus titik
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── GET: Ambil semua titik kerusakan ──────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
$res = $conn->query(
|
||||
"SELECT id, nama_kerusakan, tingkat_kerusakan, deskripsi,
|
||||
latitude, longitude, created_at, updated_at
|
||||
FROM kerusakan_jalan
|
||||
ORDER BY created_at DESC"
|
||||
);
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['latitude'] = (float) $row['latitude'];
|
||||
$row['longitude'] = (float) $row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── POST: Tambah titik kerusakan baru ─────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_kerusakan'] ?? '');
|
||||
$tingkat = $body['tingkat_kerusakan'] ?? '';
|
||||
$deskripsi = trim($body['deskripsi'] ?? '');
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
// Validasi tingkat kerusakan
|
||||
$allowedTingkat = ['Rusak Berat', 'Rusak Sedang', 'Rusak Ringan'];
|
||||
if (!in_array($tingkat, $allowedTingkat)) {
|
||||
$tingkat = 'Rusak Sedang';
|
||||
}
|
||||
|
||||
if (!$nama || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Nama dan koordinat wajib diisi']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO kerusakan_jalan (nama_kerusakan, tingkat_kerusakan, deskripsi, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssdd', $nama, $tingkat, $deskripsi, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Titik kerusakan berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── PUT: Update titik kerusakan ───────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_kerusakan'] ?? '');
|
||||
$tingkat = $body['tingkat_kerusakan'] ?? '';
|
||||
$deskripsi = trim($body['deskripsi'] ?? '');
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
$allowedTingkat = ['Rusak Berat', 'Rusak Sedang', 'Rusak Ringan'];
|
||||
if (!in_array($tingkat, $allowedTingkat)) {
|
||||
$tingkat = 'Rusak Sedang';
|
||||
}
|
||||
|
||||
if (!$nama || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE kerusakan_jalan
|
||||
SET nama_kerusakan=?, tingkat_kerusakan=?, deskripsi=?, latitude=?, longitude=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssddi', $nama, $tingkat, $deskripsi, $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Titik kerusakan berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── DELETE: Hapus titik kerusakan ─────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM kerusakan_jalan WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Titik kerusakan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api_parsil.php – REST API CRUD Data Parsil Tanah (Polygon)
|
||||
// GET api_parsil.php → semua data parsil
|
||||
// GET api_parsil.php?id=N → satu data parsil
|
||||
// POST api_parsil.php → tambah parsil
|
||||
// PUT api_parsil.php?id=N → update parsil
|
||||
// DELETE api_parsil.php?id=N → hapus parsil
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── GET ──────────────────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
|
||||
if ($id) {
|
||||
$stmt = $conn->prepare("SELECT * FROM parsil_tanah WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if ($row) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['luas_meter2'] = (float) $row['luas_meter2'];
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
echo json_encode(['status' => 'success', 'data' => $row]);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$res = $conn->query("SELECT * FROM parsil_tanah ORDER BY created_at DESC");
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['luas_meter2'] = (float) $row['luas_meter2'];
|
||||
$row['geojson'] = json_decode($row['geojson']);
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
}
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── POST (Tambah) ─────────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_pemilik'] ?? '');
|
||||
$no_sert = trim($body['no_sertifikat'] ?? '');
|
||||
$status = $body['status_sertifikat'] ?? '';
|
||||
$luas = (float) ($body['luas_meter2'] ?? 0);
|
||||
$geojson = isset($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: null;
|
||||
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!$nama || !$no_sert || !in_array($status, $valid_status) || !$geojson || $luas <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO parsil_tanah (nama_pemilik, no_sertifikat, status_sertifikat, luas_meter2, geojson)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssds', $nama, $no_sert, $status, $luas, $geojson);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Data parsil berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── PUT (Update) ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_pemilik'] ?? '');
|
||||
$no_sert = trim($body['no_sertifikat'] ?? '');
|
||||
$status = $body['status_sertifikat'] ?? '';
|
||||
$luas = (float) ($body['luas_meter2'] ?? 0);
|
||||
$geojson = isset($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: null;
|
||||
|
||||
$valid_status = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!$nama || !$no_sert || !in_array($status, $valid_status) || !$geojson || $luas <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE parsil_tanah
|
||||
SET nama_pemilik=?, no_sertifikat=?, status_sertifikat=?, luas_meter2=?, geojson=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssdsi', $nama, $no_sert, $status, $luas, $geojson, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data parsil berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── DELETE ────────────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM parsil_tanah WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'Data parsil berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config.php – Konfigurasi Koneksi Database
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_jalan_parsil');
|
||||
|
||||
function getConnection(): mysqli {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]));
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
-- ============================================================
|
||||
-- WebGIS Manajemen Jalan & Parsil Tanah
|
||||
-- Database Structure
|
||||
-- Geometri disimpan dalam format GeoJSON (TEXT)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_jalan_parsil
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_jalan_parsil;
|
||||
|
||||
-- ============================================================
|
||||
-- Tabel: jalan (Data Line / Polyline)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS jalan (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_jalan VARCHAR(200) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL,
|
||||
panjang_meter DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 'Dihitung otomatis oleh LeafletJS (meter)',
|
||||
geojson TEXT NOT NULL COMMENT 'Geometri GeoJSON LineString',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Data jalan berupa polyline GeoJSON';
|
||||
|
||||
-- ============================================================
|
||||
-- Tabel: parsil_tanah (Data Polygon)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS parsil_tanah (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_pemilik VARCHAR(200) NOT NULL,
|
||||
no_sertifikat VARCHAR(100) NOT NULL,
|
||||
status_sertifikat ENUM('SHM','HGB','HGU','HP') NOT NULL,
|
||||
luas_meter2 DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT 'Dihitung otomatis oleh LeafletJS (m²)',
|
||||
geojson TEXT NOT NULL COMMENT 'Geometri GeoJSON Polygon',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Data parsil tanah berupa polygon GeoJSON';
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data Jalan (di sekitar Pontianak)
|
||||
-- ============================================================
|
||||
INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES
|
||||
(
|
||||
'Jl. Ahmad Yani', 'Nasional', 2450.50,
|
||||
'{"type":"LineString","coordinates":[[109.3300,-0.0200],[109.3380,-0.0240],[109.3450,-0.0270],[109.3520,-0.0290]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Sultan Syarif Abdurrahman', 'Provinsi', 1820.30,
|
||||
'{"type":"LineString","coordinates":[[109.3250,-0.0350],[109.3310,-0.0330],[109.3370,-0.0310],[109.3420,-0.0290]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Parit H. Husin II', 'Kabupaten', 980.75,
|
||||
'{"type":"LineString","coordinates":[[109.3480,-0.0420],[109.3510,-0.0400],[109.3540,-0.0380]]}'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data Parsil Tanah (di sekitar Pontianak)
|
||||
-- ============================================================
|
||||
INSERT INTO parsil_tanah (nama_pemilik, no_sertifikat, status_sertifikat, luas_meter2, geojson) VALUES
|
||||
(
|
||||
'Budi Santoso', 'SHM-PTK-001234', 'SHM', 520.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.3320,-0.0280],[109.3330,-0.0280],[109.3330,-0.0290],[109.3320,-0.0290],[109.3320,-0.0280]]]}'
|
||||
),
|
||||
(
|
||||
'PT. Maju Jaya', 'HGB-PTK-005678', 'HGB', 1250.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.3390,-0.0320],[109.3410,-0.0320],[109.3410,-0.0335],[109.3390,-0.0335],[109.3390,-0.0320]]]}'
|
||||
),
|
||||
(
|
||||
'Koperasi Tani Makmur', 'HGU-PTK-009012', 'HGU', 3800.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.3460,-0.0380],[109.3490,-0.0380],[109.3490,-0.0405],[109.3460,-0.0405],[109.3460,-0.0380]]]}'
|
||||
);
|
||||
+1311
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
-- ============================================================
|
||||
-- MIGRATION: Tambah Tabel Kerusakan Jalan
|
||||
-- Jalankan query ini di database webgis_jalan_parsil yang sudah ada
|
||||
-- (JANGAN jalankan ulang database.sql yang lama, cukup file ini saja)
|
||||
-- ============================================================
|
||||
|
||||
USE webgis_jalan_parsil;
|
||||
|
||||
-- ============================================================
|
||||
-- Tabel: kerusakan_jalan (Data Point / Marker)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS kerusakan_jalan (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_kerusakan VARCHAR(250) NOT NULL COMMENT 'Nama atau deskripsi singkat lokasi kerusakan',
|
||||
tingkat_kerusakan ENUM('Rusak Berat','Rusak Sedang','Rusak Ringan') NOT NULL DEFAULT 'Rusak Sedang',
|
||||
deskripsi TEXT COMMENT 'Deskripsi detail kondisi kerusakan (opsional)',
|
||||
latitude DECIMAL(10,7) NOT NULL COMMENT 'Koordinat lintang (dari klik peta)',
|
||||
longitude DECIMAL(10,7) NOT NULL COMMENT 'Koordinat bujur (dari klik peta)',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_tingkat (tingkat_kerusakan),
|
||||
INDEX idx_coords (latitude, longitude)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Titik kerusakan jalan berupa point marker';
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data Kerusakan Jalan (di sekitar Pontianak)
|
||||
-- ============================================================
|
||||
INSERT INTO kerusakan_jalan (nama_kerusakan, tingkat_kerusakan, deskripsi, latitude, longitude) VALUES
|
||||
(
|
||||
'Lubang Besar Jl. Ahmad Yani KM3',
|
||||
'Rusak Berat',
|
||||
'Lubang berdiameter ±1.5 meter, kedalaman 30cm, berbahaya untuk kendaraan roda dua.',
|
||||
-0.0240, 109.3380
|
||||
),
|
||||
(
|
||||
'Retakan Panjang Jl. Sultan Syarif',
|
||||
'Rusak Sedang',
|
||||
'Retakan memanjang sepanjang ±20 meter di bahu jalan, perlu penanganan segera.',
|
||||
-0.0330, 109.3310
|
||||
),
|
||||
(
|
||||
'Aspal Terkelupas Jl. Parit H. Husin',
|
||||
'Rusak Ringan',
|
||||
'Permukaan aspal terkelupas dan aus, belum membentuk lubang namun licin saat hujan.',
|
||||
-0.0400, 109.3510
|
||||
);
|
||||
@@ -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
+198
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<base target="_top">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Choropleth Kepadatan Penduduk - Kota Pontianak</title>
|
||||
|
||||
<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;
|
||||
}
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.info {
|
||||
padding: 8px 12px;
|
||||
font: 14px/16px Arial, Helvetica, sans-serif;
|
||||
background: white;
|
||||
background: rgba(255,255,255,0.9);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info h4 {
|
||||
margin: 0 0 5px;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.info .kecamatan-name {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
color: #222;
|
||||
}
|
||||
.info .density-val {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.legend {
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
color: #444;
|
||||
font: 13px Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.legend i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
opacity: 0.8;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.legend .title {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #555;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script type="text/javascript" src="pontianak.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// Inisialisasi peta dengan koordinat pusat Kota Pontianak
|
||||
const map = L.map('map').setView([-0.02655429055939295, 109.34202184397597], 13);
|
||||
|
||||
// Tile layer OpenStreetMap
|
||||
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);
|
||||
|
||||
// Control info yang muncul saat hover
|
||||
const info = L.control();
|
||||
|
||||
info.onAdd = function (map) {
|
||||
this._div = L.DomUtil.create('div', 'info');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
|
||||
info.update = function (props) {
|
||||
if (props) {
|
||||
this._div.innerHTML = `
|
||||
<h4>Kepadatan Penduduk</h4>
|
||||
<div class="kecamatan-name">${props.name}</div>
|
||||
<div class="density-val">${props.density.toLocaleString('id-ID')} jiwa / km²</div>
|
||||
`;
|
||||
} else {
|
||||
this._div.innerHTML = `<h4>Kepadatan Penduduk</h4><span style="color:#999;font-size:13px">Arahkan kursor ke kecamatan</span>`;
|
||||
}
|
||||
};
|
||||
|
||||
info.addTo(map);
|
||||
|
||||
// Warna berdasarkan kepadatan penduduk (jiwa/km²)
|
||||
function getColor(d) {
|
||||
return d > 10000 ? '#800026' :
|
||||
d > 8000 ? '#BD0026' :
|
||||
d > 6000 ? '#E31A1C' :
|
||||
d > 4000 ? '#FC4E2A' :
|
||||
d > 3000 ? '#FD8D3C' :
|
||||
d > 2000 ? '#FEB24C' :
|
||||
d > 1000 ? '#FED976' : '#FFEDA0';
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
dashArray: '3',
|
||||
fillOpacity: 0.75,
|
||||
fillColor: getColor(feature.properties.density)
|
||||
};
|
||||
}
|
||||
|
||||
function highlightFeature(e) {
|
||||
const layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: 4,
|
||||
color: '#444',
|
||||
dashArray: '',
|
||||
fillOpacity: 0.85
|
||||
});
|
||||
layer.bringToFront();
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
/* global pontianakData */
|
||||
const geojson = L.geoJson(pontianakData, {
|
||||
style,
|
||||
onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
function resetHighlight(e) {
|
||||
geojson.resetStyle(e.target);
|
||||
info.update();
|
||||
}
|
||||
|
||||
function zoomToFeature(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({
|
||||
mouseover: highlightFeature,
|
||||
mouseout: resetHighlight,
|
||||
click: zoomToFeature
|
||||
});
|
||||
}
|
||||
|
||||
map.attributionControl.addAttribution('Data Wilayah © BPS Kota Pontianak');
|
||||
|
||||
// Legend
|
||||
const legend = L.control({position: 'bottomright'});
|
||||
|
||||
legend.onAdd = function (map) {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
const grades = [0, 1000, 2000, 3000, 4000, 6000, 8000, 10000];
|
||||
const labels = ['<span class="title">Jiwa / km²</span>'];
|
||||
let from, to;
|
||||
|
||||
for (let i = 0; i < grades.length; i++) {
|
||||
from = grades[i];
|
||||
to = grades[i + 1];
|
||||
labels.push(
|
||||
`<i style="background:${getColor(from + 1)}"></i> ` +
|
||||
from.toLocaleString('id-ID') +
|
||||
(to ? `–${to.toLocaleString('id-ID')}` : '+')
|
||||
);
|
||||
}
|
||||
|
||||
div.innerHTML = labels.join('<br>');
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</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
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api.php – REST API untuk WebGIS SPBU Pontianak
|
||||
// Endpoint:
|
||||
// GET api.php → ambil semua SPBU
|
||||
// POST api.php → tambah SPBU baru
|
||||
// PUT api.php?id=N → update SPBU
|
||||
// DELETE api.php?id=N → hapus SPBU
|
||||
// ============================================================
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
// ── Ambil semua SPBU ─────────────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$conn = getConnection();
|
||||
$res = $conn->query(
|
||||
"SELECT id, nama_spbu, nomor_wa, status_24jam,
|
||||
latitude, longitude, created_at
|
||||
FROM spbu ORDER BY created_at DESC"
|
||||
);
|
||||
$data = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['latitude'] = (float) $row['latitude'];
|
||||
$row['longitude'] = (float) $row['longitude'];
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Tambah SPBU baru ─────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->bind_param('sssdd', $nama, $wa, $status, $lat, $lng);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'SPBU berhasil ditambahkan',
|
||||
'id' => $stmt->insert_id
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Update SPBU ──────────────────────────────────────────────
|
||||
if ($method === 'PUT' && $id) {
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nama = trim($body['nama_spbu'] ?? '');
|
||||
$wa = trim($body['nomor_wa'] ?? '');
|
||||
$status = in_array($body['status_24jam'] ?? '', ['yes','no'])
|
||||
? $body['status_24jam'] : 'no';
|
||||
$lat = isset($body['latitude']) ? (float) $body['latitude'] : null;
|
||||
$lng = isset($body['longitude']) ? (float) $body['longitude'] : null;
|
||||
|
||||
if (!$nama || !$wa || $lat === null || $lng === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare(
|
||||
"UPDATE spbu SET nama_spbu=?, nomor_wa=?, status_24jam=?,
|
||||
latitude=?, longitude=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->bind_param('sssddi', $nama, $wa, $status, $lat, $lng, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil diupdate']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ───────────────────────────────────────────────
|
||||
if ($method === 'DELETE' && $id) {
|
||||
$conn = getConnection();
|
||||
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'SPBU berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $stmt->error]);
|
||||
}
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan']);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config.php – Konfigurasi Koneksi Database
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_spbu');
|
||||
|
||||
function getConnection(): mysqli {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Koneksi database gagal: ' . $conn->connect_error
|
||||
]));
|
||||
}
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- WebGIS SPBU Pontianak
|
||||
-- Database Structure
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(150) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
status_24jam ENUM('yes','no') NOT NULL DEFAULT 'no',
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- Sample data – beberapa SPBU di Pontianak
|
||||
-- ============================================================
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, status_24jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Jl. Ahmad Yani', '6281234567890', 'yes', -0.0262619, 109.3425923),
|
||||
('SPBU Shell Jl. Tanjungpura', '6281298765432', 'no', -0.0318750, 109.3319870),
|
||||
('SPBU Pertamina Jl. Gajahmada', '6281211223344', 'yes', -0.0393160, 109.3266190),
|
||||
('SPBU Pertamina Jl. Sultan Hamid II', '6281355667788', 'no', -0.0159870, 109.3571200);
|
||||
+1030
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
Options -Indexes
|
||||
Options -MultiViews
|
||||
|
||||
# ── Security headers ──
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
</IfModule>
|
||||
|
||||
# ── Block direct access to sensitive files ──
|
||||
<FilesMatch "^(config\.php|database\.sql|\.env)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# ── Allow uploads folder but block PHP execution inside it ──
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Block PHP execution in uploads folder
|
||||
RewriteRule ^uploads/.*\.php$ - [F,L]
|
||||
|
||||
# Route all API requests through index if needed
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^api/([^/]+)/?$ api/$1.php [L,QSA]
|
||||
</IfModule>
|
||||
|
||||
# ── Cache static assets ──
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType image/webp "access plus 1 month"
|
||||
</IfModule>
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql \
|
||||
&& a2enmod rewrite
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
RUN mkdir -p /var/www/html/uploads/bukti \
|
||||
&& chown -R www-data:www-data /var/www/html/uploads
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# WebGIS Pengentasan Kemiskinan v3.0
|
||||
## Kota Pontianak — Berbasis Komunitas & Rumah Ibadah
|
||||
|
||||
Sistem WebGIS berbasis PHP + MySQL + Leaflet.js untuk pemetaan, pendataan, dan distribusi bantuan sosial kepada penduduk miskin di Kota Pontianak.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Struktur Folder
|
||||
|
||||
```
|
||||
webgis_v3/
|
||||
├── index.php ← Halaman utama (SPA: login + semua fitur)
|
||||
├── config.php ← Konfigurasi database & helper functions
|
||||
├── database.sql ← Skema database + data demo
|
||||
├── .htaccess ← Konfigurasi Apache (keamanan + routing)
|
||||
├── uploads/ ← Foto laporan masyarakat
|
||||
└── api/
|
||||
├── auth.php ← Login / logout / cek sesi
|
||||
├── penduduk.php ← CRUD data penduduk + validasi pendidikan
|
||||
├── keluarga.php ← CRUD data keluarga (KK)
|
||||
├── rumah_ibadah.php ← CRUD rumah ibadah
|
||||
├── bantuan.php ← Master bantuan + histori + prioritas otomatis
|
||||
├── laporan.php ← Laporan masyarakat + upload foto
|
||||
├── chat.php ← Chat antar pengurus
|
||||
├── stats.php ← Dashboard KPI + GeoJSON untuk peta
|
||||
├── pelatihan.php ← Manajemen pelatihan keterampilan
|
||||
├── user.php ← Manajemen user & role (superadmin)
|
||||
└── log.php ← Log aktivitas sistem
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cara Menjalankan di XAMPP
|
||||
|
||||
### Langkah 1 — Ekstrak & Taruh di htdocs
|
||||
```
|
||||
Ekstrak ZIP → salin folder webgis_v3 ke:
|
||||
C:\xampp\htdocs\webgis_v3\
|
||||
```
|
||||
|
||||
### Langkah 2 — Start XAMPP
|
||||
Buka XAMPP Control Panel → Start **Apache** dan **MySQL**
|
||||
|
||||
### Langkah 3 — Import Database
|
||||
1. Buka browser → `http://localhost/phpmyadmin`
|
||||
2. Klik tab **SQL** di bagian atas
|
||||
3. Salin seluruh isi file `database.sql`
|
||||
4. Tempel ke kotak SQL → klik **Go**
|
||||
|
||||
Database `webgis_kemiskinan` akan terbuat otomatis dengan semua tabel dan data demo.
|
||||
|
||||
### Langkah 4 — Cek config.php (jika perlu)
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root'); // default XAMPP
|
||||
define('DB_PASS', ''); // default XAMPP = kosong
|
||||
define('DB_NAME', 'webgis_kemiskinan');
|
||||
```
|
||||
|
||||
### Langkah 5 — Buka Aplikasi
|
||||
```
|
||||
http://localhost/webgis_v3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 👥 Akun Demo
|
||||
|
||||
| Role | Username | Password | Akses |
|
||||
|------|----------|----------|-------|
|
||||
| 👑 Super Admin | `superadmin` | `password` | Semua fitur + manajemen user |
|
||||
| 🏛️ Pimpinan | `pimpinan1` | `password` | Dashboard monitoring (read-only) |
|
||||
| 🕌 Pengurus RI | `pengurus1` | `password` | Input data + distribusi bantuan |
|
||||
| 🕌 Pengurus RI | `pengurus2` | `password` | Input data + distribusi bantuan |
|
||||
| 🚑 Relawan | `relawan1` | `password` | Survey lapangan + laporan |
|
||||
| 👤 Masyarakat | `masyarakat1` | `password` | Lihat peta + buat laporan |
|
||||
|
||||
> Laporan masyarakat bisa dikirim **tanpa login** (klik chip "Lapor Tanpa Login" di halaman login).
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Struktur Database (13 Tabel)
|
||||
|
||||
| Tabel | Deskripsi |
|
||||
|-------|-----------|
|
||||
| `role` | 5 role sistem |
|
||||
| `user` | Akun pengguna + relasi ke role & rumah ibadah |
|
||||
| `rumah_ibadah` | Data masjid, gereja, vihara, klenteng, pura |
|
||||
| `keluarga` | Data Kartu Keluarga (KK) |
|
||||
| `penduduk` | Data penduduk lengkap (NIK, umur otomatis, profil umur) |
|
||||
| `pelatihan` | Program pelatihan keterampilan |
|
||||
| `riwayat_pelatihan` | Peserta per pelatihan |
|
||||
| `bantuan` | Master program bantuan (PKH, BPNT, BLT, dll) |
|
||||
| `histori_bantuan` | Riwayat penyaluran bantuan per penduduk |
|
||||
| `laporan` | Laporan kondisi warga dari masyarakat |
|
||||
| `chat` | Pesan koordinasi antar pengurus |
|
||||
| `log_aktivitas` | Audit trail semua aktivitas sistem |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Fitur Utama
|
||||
|
||||
### 🗺️ WebGIS Interaktif
|
||||
- Peta gelap (dark mode) berbasis OpenStreetMap + CartoDB
|
||||
- **3 warna marker**: 🔴 Miskin | 🟡 Rentan | 🟢 Mampu
|
||||
- Marker laporan masyarakat dengan kode urgensi
|
||||
- Layer toggle per kategori
|
||||
- Radius distribusi adjustable (100–1000m)
|
||||
- Popup detail klik marker
|
||||
|
||||
### 👥 Manajemen Data Penduduk
|
||||
- NIK 16 digit sebagai primary key
|
||||
- **Umur dihitung otomatis** dari tanggal lahir (MySQL Generated Column)
|
||||
- **Profil umur otomatis**: Balita / Anak / Remaja / Dewasa / Lansia
|
||||
- Validasi pendidikan vs umur:
|
||||
- Usia 7–15 → peringatan jika tidak sekolah
|
||||
- Usia 16–18 → peringatan jika tidak sekolah
|
||||
- Relasi ke Kartu Keluarga
|
||||
|
||||
### 🏠 Manajemen Keluarga
|
||||
- Data KK lengkap (No KK, RT/RW, kelurahan, kecamatan)
|
||||
- Relasi ke rumah ibadah terdekat
|
||||
- Daftar anggota keluarga dengan detail kondisi
|
||||
|
||||
### 🎁 Sistem Bantuan
|
||||
- **Prioritas otomatis**: miskin + sakit = SANGAT TINGGI; lansia sendiri = SANGAT TINGGI
|
||||
- Deteksi warga yang belum pernah menerima bantuan
|
||||
- Histori penyaluran bantuan per individu yang transparan
|
||||
- Catat distribusi langsung dari halaman bantuan
|
||||
|
||||
### 📢 Laporan Masyarakat
|
||||
- Bisa dikirim **tanpa login**
|
||||
- Upload foto dokumentasi (JPG/PNG, maks 5MB)
|
||||
- 4 tingkat urgensi: Rendah / Sedang / Tinggi / 🆘 Darurat
|
||||
- Alur status: Pending → Diverifikasi → Diproses → Selesai
|
||||
- Koordinat laporan via klik peta atau GPS
|
||||
|
||||
### 💬 Chat Koordinasi
|
||||
- Chat realtime antar pengurus rumah ibadah
|
||||
- Polling otomatis setiap 8 detik
|
||||
- Notifikasi dot merah jika ada pesan baru
|
||||
- Pesan broadcast ke semua pengurus
|
||||
|
||||
### 📊 Dashboard & Statistik
|
||||
- KPI card: total penduduk, KK, miskin, rentan, laporan aktif
|
||||
- Progress bar distribusi per rumah ibadah
|
||||
- Analisis singkat di sidebar
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Keamanan
|
||||
- Input sanitasi (anti XSS) di semua endpoint
|
||||
- Prepared statements PDO (anti SQL Injection)
|
||||
- Session-based auth dengan cookie HttpOnly
|
||||
- Role-based access control (RBAC) di backend
|
||||
- Block akses langsung ke `config.php` dan `database.sql` via `.htaccess`
|
||||
- Block eksekusi PHP di folder `uploads/`
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Teknologi
|
||||
| Komponen | Teknologi |
|
||||
|----------|-----------|
|
||||
| Backend | PHP 7.4+ (PDO, Sessions) |
|
||||
| Database | MySQL 5.7+ / MariaDB 10.3+ |
|
||||
| Peta | Leaflet.js v1.9.4 |
|
||||
| Tile Map | CartoDB Dark Matter (OpenStreetMap) |
|
||||
| Geocoding | Nominatim API (OSM) |
|
||||
| Frontend | Vanilla JS ES6+, CSS3 Variables |
|
||||
| Font | Plus Jakarta Sans + DM Mono |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Catatan Pengembangan Lanjutan
|
||||
1. **Backend database**: Tambah `localStorage` atau backend sinkronisasi untuk multi-device
|
||||
2. **Export laporan**: Tambah ekspor PDF/Excel menggunakan mPDF atau PhpSpreadsheet
|
||||
3. **Notifikasi push**: Integrasikan Firebase Cloud Messaging untuk notifikasi laporan darurat
|
||||
4. **PWA offline**: Service Worker untuk penggunaan lapangan tanpa internet
|
||||
5. **Integrasi DTKS**: Sinkronisasi dengan Data Terpadu Kesejahteraan Sosial Kemensos RI
|
||||
|
||||
---
|
||||
|
||||
*Dikembangkan sebagai tugas Rekayasa Perangkat Lunak — Program Studi Informatika*
|
||||
*WebGIS Pengentasan Kemiskinan Berbasis Komunitas dan Rumah Ibadah — Kota Pontianak*
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'login':
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonErr('Method not allowed', 405);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$username = sanitize($b['username'] ?? '');
|
||||
$password = $b['password'] ?? '';
|
||||
if (!$username || !$password) jsonErr('Username dan password wajib diisi.');
|
||||
|
||||
$db = getDB();
|
||||
$st = $db->prepare("SELECT u.id,u.username,u.password,u.nama_lengkap,u.telepon,u.aktif,
|
||||
r.kode as role, r.nama as role_nama, u.rumah_ibadah_id,
|
||||
ri.nama as ri_nama
|
||||
FROM user u
|
||||
JOIN role r ON r.id = u.role_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id = u.rumah_ibadah_id
|
||||
WHERE u.username = ?");
|
||||
$st->execute([$username]);
|
||||
$user = $st->fetch();
|
||||
|
||||
if (!$user) jsonErr('Username atau password salah.', 401);
|
||||
if (!$user['aktif']) jsonErr('Akun ini dinonaktifkan. Hubungi administrator.', 403);
|
||||
|
||||
$valid = password_verify($password, $user['password']) || $user['password'] === $password;
|
||||
if (!$valid) jsonErr('Username atau password salah.', 401);
|
||||
|
||||
startSession();
|
||||
$sess = [
|
||||
'id' => (int)$user['id'],
|
||||
'username' => $user['username'],
|
||||
'nama_lengkap'=> $user['nama_lengkap'],
|
||||
'role' => $user['role'],
|
||||
'role_nama' => $user['role_nama'],
|
||||
'ri_id' => $user['rumah_ibadah_id'] ? (int)$user['rumah_ibadah_id'] : null,
|
||||
'ri_nama' => $user['ri_nama'],
|
||||
];
|
||||
$_SESSION['user'] = $sess;
|
||||
log_aksi($sess['id'], 'LOGIN', 'user', $sess['id'], 'Login berhasil');
|
||||
jsonOk($sess, 'Selamat datang, '.$user['nama_lengkap'].'!');
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
$u = getCurrentUser();
|
||||
if ($u) log_aksi($u['id'], 'LOGOUT', 'user', $u['id'], 'Logout');
|
||||
startSession(); session_destroy();
|
||||
jsonOk(null, 'Logout berhasil.');
|
||||
break;
|
||||
|
||||
case 'check':
|
||||
$u = getCurrentUser();
|
||||
if ($u) jsonOk($u);
|
||||
else jsonErr('Tidak ada sesi aktif.', 401);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonErr('Action tidak dikenali.', 404);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
// ── HISTORI BANTUAN ──
|
||||
if ($action === 'histori') {
|
||||
if ($method === 'GET') {
|
||||
$pid = (int)($_GET['penduduk_id'] ?? 0);
|
||||
if (!$pid) jsonErr('penduduk_id wajib.');
|
||||
$st = $db->prepare("SELECT hb.*, b.nama as bantuan_nama, b.jenis, b.satuan, b.sumber,
|
||||
u.nama_lengkap as disalurkan_nama
|
||||
FROM histori_bantuan hb
|
||||
JOIN bantuan b ON b.id=hb.bantuan_id
|
||||
LEFT JOIN user u ON u.id=hb.disalurkan_oleh
|
||||
WHERE hb.penduduk_id=? ORDER BY hb.tanggal DESC");
|
||||
$st->execute([$pid]);
|
||||
jsonOk($st->fetchAll());
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
requireRole(['superadmin','pengurus']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$pid = (int)($b['penduduk_id'] ?? 0);
|
||||
$bid = (int)($b['bantuan_id'] ?? 0);
|
||||
if (!$pid || !$bid) jsonErr('penduduk_id dan bantuan_id wajib.');
|
||||
$db->prepare("INSERT INTO histori_bantuan (penduduk_id,bantuan_id,disalurkan_oleh,tanggal,jumlah,status,catatan) VALUES (?,?,?,?,?,?,?)")
|
||||
->execute([$pid,$bid,$user['id'],$b['tanggal']??date('Y-m-d'),(float)($b['jumlah']??1),$b['status']??'disalurkan',sanitize($b['catatan']??'')]);
|
||||
$newId=(int)$db->lastInsertId();
|
||||
log_aksi($user['id'],'CREATE','histori_bantuan',$newId,"Catat bantuan penduduk_id:$pid bantuan_id:$bid");
|
||||
jsonOk(['id'=>$newId],'Histori bantuan dicatat.');
|
||||
}
|
||||
if ($method === 'PUT') {
|
||||
requireRole(['superadmin','pengurus']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("UPDATE histori_bantuan SET status=?,catatan=? WHERE id=?")
|
||||
->execute([$b['status']??'disalurkan',sanitize($b['catatan']??''),$id]);
|
||||
jsonOk(null,'Status bantuan diperbarui.');
|
||||
}
|
||||
jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
|
||||
// ── PRIORITAS OTOMATIS ──
|
||||
if ($action === 'prioritas') {
|
||||
$st = $db->query("
|
||||
SELECT p.id, p.nik, p.nama, p.umur, p.profil_umur, p.kondisi_kesehatan,
|
||||
p.status_ekonomi, p.penghasilan, p.status_perkawinan,
|
||||
k.alamat, k.kelurahan, k.lat, k.lng,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id=p.id AND hb.status='disalurkan') as total_bantuan,
|
||||
CASE
|
||||
WHEN p.status_ekonomi='miskin' AND p.kondisi_kesehatan IN ('sakit_berat','disabilitas') THEN 'SANGAT TINGGI'
|
||||
WHEN p.profil_umur='Lansia' AND p.status_perkawinan IN ('cerai_mati','cerai_hidup') THEN 'SANGAT TINGGI'
|
||||
WHEN p.status_ekonomi='miskin' AND p.kondisi_kesehatan='sakit_ringan' THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi='miskin' AND p.profil_umur='Lansia' THEN 'TINGGI'
|
||||
WHEN p.status_ekonomi='miskin' THEN 'SEDANG'
|
||||
WHEN p.status_ekonomi='rentan' AND p.kondisi_kesehatan != 'sehat' THEN 'SEDANG'
|
||||
ELSE 'RENDAH'
|
||||
END as prioritas
|
||||
FROM v_penduduk p
|
||||
JOIN keluarga k ON k.id=p.keluarga_id
|
||||
WHERE p.status_hidup='hidup'
|
||||
ORDER BY FIELD(prioritas,'SANGAT TINGGI','TINGGI','SEDANG','RENDAH'), total_bantuan ASC
|
||||
LIMIT 100
|
||||
");
|
||||
jsonOk($st->fetchAll());
|
||||
}
|
||||
|
||||
// ── BELUM TERIMA BANTUAN ──
|
||||
if ($action === 'belum_terima') {
|
||||
$st = $db->query("
|
||||
SELECT p.id, p.nik, p.nama, p.umur, p.profil_umur, p.status_ekonomi,
|
||||
p.kondisi_kesehatan, k.kelurahan, k.alamat, k.lat, k.lng
|
||||
FROM v_penduduk p
|
||||
JOIN keluarga k ON k.id=p.keluarga_id
|
||||
WHERE p.status_hidup='hidup'
|
||||
AND p.status_ekonomi IN ('miskin','rentan')
|
||||
AND p.id NOT IN (SELECT DISTINCT penduduk_id FROM histori_bantuan WHERE status='disalurkan')
|
||||
ORDER BY p.status_ekonomi DESC, p.umur DESC
|
||||
");
|
||||
jsonOk($st->fetchAll());
|
||||
}
|
||||
|
||||
// ── CRUD MASTER BANTUAN ──
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT * FROM bantuan WHERE id=?"); $st->execute([$id]);
|
||||
jsonOk($st->fetch());
|
||||
}
|
||||
jsonOk($db->query("SELECT * FROM bantuan ORDER BY nama")->fetchAll());
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
requireRole(['superadmin','pengurus']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("INSERT INTO bantuan (nama,jenis,sumber,satuan,nilai_per_satuan,periode) VALUES (?,?,?,?,?,?)")
|
||||
->execute([sanitize($b['nama']??''),$b['jenis']??'lainnya',sanitize($b['sumber']??''),sanitize($b['satuan']??''),(float)($b['nilai_per_satuan']??0),$b['periode']??'sekali']);
|
||||
jsonOk(['id'=>(int)$db->lastInsertId()],'Bantuan ditambahkan.');
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireRole(['superadmin','pengurus']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("UPDATE bantuan SET nama=?,jenis=?,sumber=?,satuan=?,nilai_per_satuan=?,periode=?,aktif=? WHERE id=?")
|
||||
->execute([sanitize($b['nama']??''),$b['jenis']??'lainnya',sanitize($b['sumber']??''),sanitize($b['satuan']??''),(float)($b['nilai_per_satuan']??0),$b['periode']??'sekali',(int)($b['aktif']??1),$id]);
|
||||
jsonOk(null,'Bantuan diperbarui.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$since = $_GET['since'] ?? '1970-01-01 00:00:00';
|
||||
// ambil pesan broadcast + pesan ke/dari user ini
|
||||
$st = $db->prepare("SELECT c.id, c.pesan, c.created_at, c.dibaca,
|
||||
u.nama_lengkap as dari_nama, u.id as dari_id,
|
||||
r.kode as dari_role, u.rumah_ibadah_id,
|
||||
ri.nama as dari_ri,
|
||||
c.ke_user_id
|
||||
FROM chat c
|
||||
JOIN user u ON u.id=c.dari_user_id
|
||||
JOIN role r ON r.id=u.role_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id
|
||||
WHERE (c.ke_user_id IS NULL OR c.ke_user_id=? OR c.dari_user_id=?)
|
||||
AND c.created_at > ?
|
||||
ORDER BY c.created_at ASC LIMIT 100");
|
||||
$st->execute([$user['id'], $user['id'], $since]);
|
||||
$msgs = $st->fetchAll();
|
||||
|
||||
// tandai sudah dibaca
|
||||
$db->prepare("UPDATE chat SET dibaca=1 WHERE ke_user_id=? AND dibaca=0")->execute([$user['id']]);
|
||||
jsonOk($msgs);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
// hanya pengurus, relawan, superadmin
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$pesan = sanitize($b['pesan'] ?? '');
|
||||
if (!$pesan) jsonErr('Pesan tidak boleh kosong.');
|
||||
$ke = !empty($b['ke_user_id']) ? (int)$b['ke_user_id'] : null;
|
||||
|
||||
$db->prepare("INSERT INTO chat (dari_user_id, ke_user_id, pesan) VALUES (?,?,?)")
|
||||
->execute([$user['id'], $ke, $pesan]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
|
||||
// ambil pesan lengkap untuk response
|
||||
$st = $db->prepare("SELECT c.id, c.pesan, c.created_at, u.nama_lengkap as dari_nama,
|
||||
r.kode as dari_role, ri.nama as dari_ri, c.ke_user_id
|
||||
FROM chat c JOIN user u ON u.id=c.dari_user_id
|
||||
JOIN role r ON r.id=u.role_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id
|
||||
WHERE c.id=?");
|
||||
$st->execute([$newId]);
|
||||
jsonOk($st->fetch(), 'Pesan terkirim.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT k.*, ri.nama as ri_nama, ri.jenis as ri_jenis
|
||||
FROM keluarga k LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
|
||||
WHERE k.id=?");
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) jsonErr('Tidak ditemukan.', 404);
|
||||
|
||||
// anggota keluarga
|
||||
$an = $db->prepare("SELECT id,nik,nama,jenis_kelamin,umur,profil_umur,
|
||||
status_keluarga,status_perkawinan,status_ekonomi,
|
||||
kondisi_kesehatan,pekerjaan,penghasilan,
|
||||
status_pendidikan,pendidikan_terakhir
|
||||
FROM penduduk WHERE keluarga_id=? AND status_hidup='hidup' ORDER BY
|
||||
FIELD(status_keluarga,'kepala','istri','anak','orang_tua','lainnya')");
|
||||
$an->execute([$id]);
|
||||
$row['anggota'] = $an->fetchAll();
|
||||
jsonOk($row);
|
||||
break;
|
||||
}
|
||||
|
||||
$where = []; $params = [];
|
||||
if (!empty($_GET['status_ekonomi'])) { $where[] = "k.status_ekonomi=?"; $params[] = $_GET['status_ekonomi']; }
|
||||
if (!empty($_GET['kelurahan'])) { $where[] = "k.kelurahan=?"; $params[] = $_GET['kelurahan']; }
|
||||
if (!empty($_GET['rumah_ibadah_id'])) { $where[] = "k.rumah_ibadah_id=?"; $params[] = (int)$_GET['rumah_ibadah_id']; }
|
||||
if (!empty($_GET['q'])) { $where[] = "(k.nama_kk LIKE ? OR k.no_kk LIKE ?)"; $params[] = '%'.$_GET['q'].'%'; $params[] = '%'.$_GET['q'].'%'; }
|
||||
|
||||
$wStr = $where ? 'WHERE '.implode(' AND ',$where) : '';
|
||||
$st = $db->prepare("SELECT k.id, k.no_kk, k.nama_kk, k.alamat, k.rt, k.rw,
|
||||
k.kelurahan, k.kecamatan, k.lat, k.lng, k.status_ekonomi,
|
||||
ri.nama as ri_nama,
|
||||
(SELECT COUNT(*) FROM penduduk p WHERE p.keluarga_id=k.id AND p.status_hidup='hidup') as jumlah_jiwa,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb JOIN penduduk p ON p.id=hb.penduduk_id WHERE p.keluarga_id=k.id AND hb.status='disalurkan') as total_bantuan_diterima
|
||||
FROM keluarga k LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
|
||||
$wStr ORDER BY k.nama_kk ASC LIMIT 500");
|
||||
$st->execute($params);
|
||||
jsonOk($st->fetchAll());
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$no_kk = sanitize($b['no_kk'] ?? '');
|
||||
$nama_kk = sanitize($b['nama_kk'] ?? '');
|
||||
if (!$no_kk || !$nama_kk) jsonErr('No KK dan nama KK wajib diisi.');
|
||||
if (strlen($no_kk) !== 16) jsonErr('No KK harus 16 digit.');
|
||||
|
||||
$chk = $db->prepare("SELECT id FROM keluarga WHERE no_kk=?"); $chk->execute([$no_kk]);
|
||||
if ($chk->fetch()) jsonErr('No KK sudah terdaftar.');
|
||||
|
||||
$db->prepare("INSERT INTO keluarga (no_kk,nama_kk,alamat,rt,rw,kelurahan,kecamatan,lat,lng,rumah_ibadah_id,status_ekonomi,created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||
->execute([
|
||||
$no_kk, $nama_kk,
|
||||
sanitize($b['alamat'] ?? ''),
|
||||
sanitize($b['rt'] ?? ''), sanitize($b['rw'] ?? ''),
|
||||
sanitize($b['kelurahan'] ?? ''), sanitize($b['kecamatan'] ?? ''),
|
||||
isset($b['lat']) ? (float)$b['lat'] : null,
|
||||
isset($b['lng']) ? (float)$b['lng'] : null,
|
||||
!empty($b['rumah_ibadah_id']) ? (int)$b['rumah_ibadah_id'] : null,
|
||||
$b['status_ekonomi'] ?? 'rentan',
|
||||
$user['id']
|
||||
]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
log_aksi($user['id'], 'CREATE', 'keluarga', $newId, "Tambah KK: $nama_kk");
|
||||
jsonOk(['id'=>$newId], 'Keluarga berhasil ditambahkan.');
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("UPDATE keluarga SET nama_kk=?,alamat=?,rt=?,rw=?,kelurahan=?,kecamatan=?,
|
||||
lat=?,lng=?,rumah_ibadah_id=?,status_ekonomi=?,updated_at=NOW() WHERE id=?")
|
||||
->execute([
|
||||
sanitize($b['nama_kk'] ?? ''), sanitize($b['alamat'] ?? ''),
|
||||
sanitize($b['rt'] ?? ''), sanitize($b['rw'] ?? ''),
|
||||
sanitize($b['kelurahan'] ?? ''), sanitize($b['kecamatan'] ?? ''),
|
||||
isset($b['lat']) ? (float)$b['lat'] : null,
|
||||
isset($b['lng']) ? (float)$b['lng'] : null,
|
||||
!empty($b['rumah_ibadah_id']) ? (int)$b['rumah_ibadah_id'] : null,
|
||||
$b['status_ekonomi'] ?? 'rentan',
|
||||
$id
|
||||
]);
|
||||
log_aksi($user['id'], 'UPDATE', 'keluarga', $id, "Edit KK ID: $id");
|
||||
jsonOk(null, 'Data keluarga diperbarui.');
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requireRole(['superadmin']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$db->prepare("DELETE FROM keluarga WHERE id=?")->execute([$id]);
|
||||
log_aksi($user['id'], 'DELETE', 'keluarga', $id, "Hapus KK ID: $id");
|
||||
jsonOk(null, 'Data keluarga dihapus.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT l.*, p.nama as penduduk_nama, u.nama_lengkap as verifikator
|
||||
FROM laporan l
|
||||
LEFT JOIN penduduk p ON p.id=l.penduduk_id
|
||||
LEFT JOIN user u ON u.id=l.diverifikasi_oleh
|
||||
WHERE l.id=?");
|
||||
$st->execute([$id]); jsonOk($st->fetch()); break;
|
||||
}
|
||||
$where=[]; $params=[];
|
||||
if (!empty($_GET['status'])) { $where[]="l.status=?"; $params[]=$_GET['status']; }
|
||||
if (!empty($_GET['urgensi'])) { $where[]="l.urgensi=?"; $params[]=$_GET['urgensi']; }
|
||||
$w = $where ? 'WHERE '.implode(' AND ',$where) : '';
|
||||
$st = $db->prepare("SELECT l.id, l.pelapor_nama, l.deskripsi, l.lat, l.lng,
|
||||
l.alamat_laporan, l.urgensi, l.status, l.foto,
|
||||
l.created_at, p.nama as penduduk_nama
|
||||
FROM laporan l LEFT JOIN penduduk p ON p.id=l.penduduk_id
|
||||
$w ORDER BY FIELD(l.urgensi,'darurat','tinggi','sedang','rendah'), l.created_at DESC");
|
||||
$st->execute($params); jsonOk($st->fetchAll()); break;
|
||||
|
||||
case 'POST':
|
||||
// masyarakat umum pun bisa lapor (tidak perlu login)
|
||||
$pelapor = sanitize($_POST['pelapor_nama'] ?? '');
|
||||
$telp = sanitize($_POST['pelapor_telepon']?? '');
|
||||
$deskripsi = sanitize($_POST['deskripsi'] ?? '');
|
||||
$alamat = sanitize($_POST['alamat_laporan'] ?? '');
|
||||
$lat = isset($_POST['lat']) ? (float)$_POST['lat'] : null;
|
||||
$lng = isset($_POST['lng']) ? (float)$_POST['lng'] : null;
|
||||
$urgensi = $_POST['urgensi'] ?? 'sedang';
|
||||
$pid = !empty($_POST['penduduk_id']) ? (int)$_POST['penduduk_id'] : null;
|
||||
|
||||
if (!$deskripsi) jsonErr('Deskripsi laporan wajib diisi.');
|
||||
|
||||
// Upload foto
|
||||
$fotoPath = null;
|
||||
if (!empty($_FILES['foto']['tmp_name'])) {
|
||||
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg','jpeg','png','webp'])) jsonErr('Format foto harus JPG/PNG/WebP.');
|
||||
if ($_FILES['foto']['size'] > 5 * 1024 * 1024) jsonErr('Ukuran foto maks 5MB.');
|
||||
$fname = 'laporan_' . time() . '_' . rand(1000,9999) . '.' . $ext;
|
||||
move_uploaded_file($_FILES['foto']['tmp_name'], UPLOAD_DIR . $fname);
|
||||
$fotoPath = 'uploads/' . $fname;
|
||||
}
|
||||
|
||||
$db->prepare("INSERT INTO laporan (pelapor_nama,pelapor_telepon,penduduk_id,deskripsi,lat,lng,alamat_laporan,foto,urgensi) VALUES (?,?,?,?,?,?,?,?,?)")
|
||||
->execute([$pelapor,$telp,$pid,$deskripsi,$lat,$lng,$alamat,$fotoPath,$urgensi]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
$u = getCurrentUser();
|
||||
if ($u) log_aksi($u['id'],'CREATE','laporan',$newId,"Laporan baru: $urgensi");
|
||||
jsonOk(['id'=>$newId],'Laporan berhasil dikirim. Terima kasih!'); break;
|
||||
|
||||
case 'PUT':
|
||||
$u = requireLogin();
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$chk=$db->prepare("SELECT id FROM laporan WHERE id=?"); $chk->execute([$id]);
|
||||
if (!$chk->fetch()) jsonErr('Laporan tidak ditemukan.',404);
|
||||
|
||||
$db->prepare("UPDATE laporan SET status=?,catatan_admin=?,diverifikasi_oleh=?,updated_at=NOW() WHERE id=?")
|
||||
->execute([$b['status']??'pending',sanitize($b['catatan_admin']??''),$u['id'],$id]);
|
||||
log_aksi($u['id'],'UPDATE','laporan',$id,"Update status: ".($b['status']??''));
|
||||
jsonOk(null,'Status laporan diperbarui.'); break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.',405);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// api/log.php — Log Aktivitas
|
||||
require_once __DIR__ . '/../config.php';
|
||||
requireRole(['superadmin', 'pimpinan']);
|
||||
$db = getDB();
|
||||
|
||||
$limit = min((int)($_GET['limit'] ?? 100), 500);
|
||||
$offset = (int)($_GET['offset'] ?? 0);
|
||||
$tabel = $_GET['tabel'] ?? '';
|
||||
$uid = (int)($_GET['user_id'] ?? 0);
|
||||
|
||||
$where = []; $params = [];
|
||||
if ($tabel) { $where[] = "l.tabel=?"; $params[] = $tabel; }
|
||||
if ($uid) { $where[] = "l.user_id=?"; $params[] = $uid; }
|
||||
$w = $where ? 'WHERE '.implode(' AND ',$where) : '';
|
||||
|
||||
$st = getDB()->prepare("SELECT l.id, l.aksi, l.tabel, l.record_id, l.keterangan, l.ip_address, l.created_at,
|
||||
u.username, u.nama_lengkap, r.kode as role
|
||||
FROM log_aktivitas l
|
||||
LEFT JOIN user u ON u.id=l.user_id
|
||||
LEFT JOIN role r ON r.id=u.role_id
|
||||
$w ORDER BY l.created_at DESC LIMIT ? OFFSET ?");
|
||||
$params[] = $limit; $params[] = $offset;
|
||||
$st->execute($params);
|
||||
|
||||
$total = (int)getDB()->query("SELECT COUNT(*) FROM log_aktivitas $w")->fetchColumn();
|
||||
jsonOk(['rows' => $st->fetchAll(), 'total' => $total, 'limit' => $limit, 'offset' => $offset]);
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// api/pelatihan.php — Manajemen Pelatihan & Riwayat
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
// ── RIWAYAT PELATIHAN PER PENDUDUK ──
|
||||
if ($action === 'riwayat') {
|
||||
$pid = (int)($_GET['penduduk_id'] ?? 0);
|
||||
if (!$pid) jsonErr('penduduk_id wajib.');
|
||||
$st = $db->prepare("SELECT rp.*, p.nama_pelatihan, p.penyelenggara, p.tanggal as tgl_pelatihan
|
||||
FROM riwayat_pelatihan rp
|
||||
JOIN pelatihan p ON p.id=rp.pelatihan_id
|
||||
WHERE rp.penduduk_id=? ORDER BY rp.tanggal_ikut DESC");
|
||||
$st->execute([$pid]);
|
||||
jsonOk($st->fetchAll());
|
||||
}
|
||||
|
||||
// ── DAFTARKAN PESERTA ──
|
||||
if ($action === 'daftar' && $method === 'POST') {
|
||||
requireRole(['superadmin', 'pengurus', 'relawan']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$pid = (int)($b['penduduk_id'] ?? 0);
|
||||
$tid = (int)($b['pelatihan_id'] ?? 0);
|
||||
if (!$pid || !$tid) jsonErr('penduduk_id dan pelatihan_id wajib.');
|
||||
|
||||
// validasi: cek umur >= 18 untuk pelatihan kerja
|
||||
$p = $db->prepare("SELECT umur FROM penduduk WHERE id=?"); $p->execute([$pid]);
|
||||
$row = $p->fetch();
|
||||
if ($row && (int)$row['umur'] < 15) jsonErr('Peserta minimal berusia 15 tahun untuk mengikuti pelatihan.');
|
||||
|
||||
$db->prepare("INSERT INTO riwayat_pelatihan (penduduk_id, pelatihan_id, tanggal_ikut) VALUES (?,?,?)")
|
||||
->execute([$pid, $tid, $b['tanggal_ikut'] ?? date('Y-m-d')]);
|
||||
log_aksi($user['id'], 'CREATE', 'riwayat_pelatihan', (int)$db->lastInsertId(), "Daftar pelatihan pid:$pid tid:$tid");
|
||||
jsonOk(null, 'Peserta berhasil didaftarkan ke pelatihan.');
|
||||
}
|
||||
|
||||
// ── CRUD MASTER PELATIHAN ──
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT p.*, COUNT(rp.id) as jumlah_peserta FROM pelatihan p
|
||||
LEFT JOIN riwayat_pelatihan rp ON rp.pelatihan_id=p.id WHERE p.id=? GROUP BY p.id");
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) jsonErr('Tidak ditemukan.', 404);
|
||||
// peserta
|
||||
$ps = $db->prepare("SELECT rp.id, rp.tanggal_ikut, pd.nama, pd.nik, pd.umur
|
||||
FROM riwayat_pelatihan rp JOIN v_penduduk pd ON pd.id=rp.penduduk_id
|
||||
WHERE rp.pelatihan_id=? ORDER BY rp.tanggal_ikut DESC");
|
||||
$ps->execute([$id]);
|
||||
$row['peserta'] = $ps->fetchAll();
|
||||
jsonOk($row);
|
||||
break;
|
||||
}
|
||||
$rows = $db->query("SELECT p.*, COUNT(rp.id) as jumlah_peserta
|
||||
FROM pelatihan p LEFT JOIN riwayat_pelatihan rp ON rp.pelatihan_id=p.id
|
||||
GROUP BY p.id ORDER BY p.tanggal DESC")->fetchAll();
|
||||
jsonOk($rows);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
requireRole(['superadmin', 'pengurus']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$nama = sanitize($b['nama_pelatihan'] ?? '');
|
||||
if (!$nama) jsonErr('Nama pelatihan wajib diisi.');
|
||||
$db->prepare("INSERT INTO pelatihan (nama_pelatihan, penyelenggara, tanggal, durasi_hari, deskripsi) VALUES (?,?,?,?,?)")
|
||||
->execute([$nama, sanitize($b['penyelenggara'] ?? ''), $b['tanggal'] ?? null,
|
||||
(int)($b['durasi_hari'] ?? 1), sanitize($b['deskripsi'] ?? '')]);
|
||||
jsonOk(['id' => (int)$db->lastInsertId()], 'Pelatihan ditambahkan.');
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireRole(['superadmin', 'pengurus']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("UPDATE pelatihan SET nama_pelatihan=?, penyelenggara=?, tanggal=?, durasi_hari=?, deskripsi=? WHERE id=?")
|
||||
->execute([sanitize($b['nama_pelatihan'] ?? ''), sanitize($b['penyelenggara'] ?? ''),
|
||||
$b['tanggal'] ?? null, (int)($b['durasi_hari'] ?? 1),
|
||||
sanitize($b['deskripsi'] ?? ''), $id]);
|
||||
jsonOk(null, 'Pelatihan diperbarui.');
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requireRole(['superadmin']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$db->prepare("DELETE FROM pelatihan WHERE id=?")->execute([$id]);
|
||||
jsonOk(null, 'Pelatihan dihapus.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
// ── GET LIST / SINGLE ──
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
// single dengan histori bantuan & riwayat pelatihan
|
||||
$st = $db->prepare("SELECT p.*, k.no_kk, k.alamat as alamat_kk, k.rt, k.rw,
|
||||
k.kelurahan, k.kecamatan, k.rumah_ibadah_id
|
||||
FROM v_penduduk p
|
||||
JOIN keluarga k ON k.id = p.keluarga_id
|
||||
WHERE p.id = ?");
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
if (!$row) jsonErr('Data tidak ditemukan.', 404);
|
||||
|
||||
// histori bantuan
|
||||
$hb = $db->prepare("SELECT hb.*, b.nama as bantuan_nama, b.jenis, b.satuan,
|
||||
u.nama_lengkap as disalurkan_nama
|
||||
FROM histori_bantuan hb
|
||||
JOIN bantuan b ON b.id = hb.bantuan_id
|
||||
LEFT JOIN user u ON u.id = hb.disalurkan_oleh
|
||||
WHERE hb.penduduk_id = ? ORDER BY hb.tanggal DESC");
|
||||
$hb->execute([$id]);
|
||||
$row['histori_bantuan'] = $hb->fetchAll();
|
||||
|
||||
// pelatihan
|
||||
$plt = $db->prepare("SELECT rp.*, p.nama_pelatihan, p.penyelenggara
|
||||
FROM riwayat_pelatihan rp
|
||||
JOIN pelatihan p ON p.id = rp.pelatihan_id
|
||||
WHERE rp.penduduk_id = ?");
|
||||
$plt->execute([$id]);
|
||||
$row['riwayat_pelatihan'] = $plt->fetchAll();
|
||||
|
||||
// validasi pendidikan
|
||||
$warn = validasiPendidikan((int)$row['umur'], $row['pendidikan_terakhir'], $row['status_pendidikan']);
|
||||
$row['peringatan_pendidikan'] = $warn;
|
||||
|
||||
jsonOk($row);
|
||||
}
|
||||
|
||||
// list dengan filter
|
||||
$where = ["p.status_hidup = 'hidup'"]; $params = [];
|
||||
if (!empty($_GET['status_ekonomi'])) { $where[] = "p.status_ekonomi = ?"; $params[] = $_GET['status_ekonomi']; }
|
||||
if (!empty($_GET['keluarga_id'])) { $where[] = "p.keluarga_id = ?"; $params[] = (int)$_GET['keluarga_id']; }
|
||||
if (!empty($_GET['kelurahan'])) { $where[] = "k.kelurahan = ?"; $params[] = $_GET['kelurahan']; }
|
||||
if (!empty($_GET['q'])) { $where[] = "(p.nama LIKE ? OR p.nik LIKE ?)"; $params[] = '%'.$_GET['q'].'%'; $params[] = '%'.$_GET['q'].'%'; }
|
||||
|
||||
$wStr = implode(' AND ', $where);
|
||||
$st = $db->prepare("SELECT p.id, p.nik, p.nama, p.jenis_kelamin, p.tanggal_lahir, p.umur,
|
||||
p.profil_umur, p.status_keluarga, p.status_perkawinan,
|
||||
p.status_ekonomi, p.kondisi_kesehatan, p.pekerjaan,
|
||||
p.penghasilan, p.lat, p.lng, p.keluarga_id,
|
||||
k.no_kk, k.kelurahan, k.alamat as alamat_kk,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id=p.id AND hb.status='disalurkan') as total_bantuan
|
||||
FROM v_penduduk p JOIN keluarga k ON k.id=p.keluarga_id
|
||||
WHERE $wStr ORDER BY p.nama ASC LIMIT 500");
|
||||
$st->execute($params);
|
||||
jsonOk($st->fetchAll());
|
||||
break;
|
||||
|
||||
// ── POST: Tambah ──
|
||||
case 'POST':
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$nik = sanitize($b['nik'] ?? '');
|
||||
$nama = sanitize($b['nama'] ?? '');
|
||||
$tgl = $b['tanggal_lahir'] ?? '';
|
||||
$kjId = (int)($b['keluarga_id'] ?? 0);
|
||||
|
||||
if (!$nik || strlen($nik) !== 16) jsonErr('NIK harus 16 digit.');
|
||||
if (!$nama) jsonErr('Nama wajib diisi.');
|
||||
if (!$tgl) jsonErr('Tanggal lahir wajib diisi.');
|
||||
if (!$kjId) jsonErr('Keluarga (KK) wajib dipilih.');
|
||||
|
||||
// cek NIK unik
|
||||
$chk = $db->prepare("SELECT id FROM penduduk WHERE nik=?"); $chk->execute([$nik]);
|
||||
if ($chk->fetch()) jsonErr('NIK sudah terdaftar di sistem.');
|
||||
|
||||
$umur = hitungUmur($tgl);
|
||||
$pendid = $b['pendidikan_terakhir'] ?? 'tidak_sekolah';
|
||||
$stPend = $b['status_pendidikan'] ?? 'tidak_sekolah';
|
||||
|
||||
$db->prepare("INSERT INTO penduduk
|
||||
(nik,nama,jenis_kelamin,tanggal_lahir,status_keluarga,status_perkawinan,status_hidup,
|
||||
keluarga_id,pendidikan_terakhir,status_pendidikan,pekerjaan,penghasilan,
|
||||
status_ekonomi,kondisi_kesehatan,lat,lng,catatan,created_by)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
|
||||
->execute([
|
||||
$nik, $nama,
|
||||
$b['jenis_kelamin'] ?? 'L',
|
||||
$tgl,
|
||||
$b['status_keluarga'] ?? 'anggota',
|
||||
$b['status_perkawinan'] ?? 'belum_kawin',
|
||||
$b['status_hidup'] ?? 'hidup',
|
||||
$kjId, $pendid, $stPend,
|
||||
sanitize($b['pekerjaan'] ?? ''),
|
||||
(float)($b['penghasilan'] ?? 0),
|
||||
$b['status_ekonomi'] ?? 'rentan',
|
||||
$b['kondisi_kesehatan'] ?? 'sehat',
|
||||
isset($b['lat']) ? (float)$b['lat'] : null,
|
||||
isset($b['lng']) ? (float)$b['lng'] : null,
|
||||
sanitize($b['catatan'] ?? ''),
|
||||
$user['id']
|
||||
]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
log_aksi($user['id'], 'CREATE', 'penduduk', $newId, "Tambah: $nama (NIK: $nik)");
|
||||
|
||||
$warn = validasiPendidikan($umur, $pendid, $stPend);
|
||||
jsonOk(['id'=>$newId,'peringatan'=>$warn], 'Penduduk berhasil ditambahkan.');
|
||||
break;
|
||||
|
||||
// ── PUT: Edit ──
|
||||
case 'PUT':
|
||||
requireRole(['superadmin','pengurus','relawan']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$chk = $db->prepare("SELECT id,nik FROM penduduk WHERE id=?"); $chk->execute([$id]);
|
||||
if (!$chk->fetch()) jsonErr('Data tidak ditemukan.', 404);
|
||||
|
||||
$tgl = $b['tanggal_lahir'] ?? '';
|
||||
$umur = $tgl ? hitungUmur($tgl) : 0;
|
||||
$pendid = $b['pendidikan_terakhir'] ?? 'tidak_sekolah';
|
||||
$stPend = $b['status_pendidikan'] ?? 'tidak_sekolah';
|
||||
|
||||
$db->prepare("UPDATE penduduk SET
|
||||
nama=?,jenis_kelamin=?,tanggal_lahir=?,status_keluarga=?,status_perkawinan=?,
|
||||
status_hidup=?,keluarga_id=?,pendidikan_terakhir=?,status_pendidikan=?,
|
||||
pekerjaan=?,penghasilan=?,status_ekonomi=?,kondisi_kesehatan=?,
|
||||
lat=?,lng=?,catatan=?,updated_at=NOW() WHERE id=?")
|
||||
->execute([
|
||||
sanitize($b['nama'] ?? ''),
|
||||
$b['jenis_kelamin'] ?? 'L',
|
||||
$tgl,
|
||||
$b['status_keluarga'] ?? 'anggota',
|
||||
$b['status_perkawinan'] ?? 'belum_kawin',
|
||||
$b['status_hidup'] ?? 'hidup',
|
||||
(int)($b['keluarga_id'] ?? 0),
|
||||
$pendid, $stPend,
|
||||
sanitize($b['pekerjaan'] ?? ''),
|
||||
(float)($b['penghasilan'] ?? 0),
|
||||
$b['status_ekonomi'] ?? 'rentan',
|
||||
$b['kondisi_kesehatan'] ?? 'sehat',
|
||||
isset($b['lat']) ? (float)$b['lat'] : null,
|
||||
isset($b['lng']) ? (float)$b['lng'] : null,
|
||||
sanitize($b['catatan'] ?? ''),
|
||||
$id
|
||||
]);
|
||||
log_aksi($user['id'], 'UPDATE', 'penduduk', $id, "Edit ID: $id");
|
||||
$warn = validasiPendidikan($umur, $pendid, $stPend);
|
||||
jsonOk(['peringatan'=>$warn], 'Data berhasil diperbarui.');
|
||||
break;
|
||||
|
||||
// ── DELETE ──
|
||||
case 'DELETE':
|
||||
requireRole(['superadmin']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$chk = $db->prepare("SELECT nama FROM penduduk WHERE id=?"); $chk->execute([$id]);
|
||||
$row = $chk->fetch();
|
||||
if (!$row) jsonErr('Data tidak ditemukan.', 404);
|
||||
$db->prepare("DELETE FROM penduduk WHERE id=?")->execute([$id]);
|
||||
log_aksi($user['id'], 'DELETE', 'penduduk', $id, 'Hapus: '.$row['nama']);
|
||||
jsonOk(null, 'Data berhasil dihapus.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireLogin();
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id) as jumlah_kk,
|
||||
(SELECT COUNT(*) FROM keluarga k JOIN penduduk p ON p.keluarga_id=k.id WHERE k.rumah_ibadah_id=ri.id AND p.status_hidup='hidup') as jumlah_jiwa,
|
||||
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id AND k.status_ekonomi='miskin') as kk_miskin
|
||||
FROM rumah_ibadah ri WHERE ri.id=?");
|
||||
$st->execute([$id]); jsonOk($st->fetch()); break;
|
||||
}
|
||||
$rows = $db->query("SELECT ri.id, ri.nama, ri.jenis, ri.kontak, ri.lat, ri.lng, ri.alamat, ri.kelurahan,
|
||||
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id) as jumlah_kk,
|
||||
(SELECT COUNT(*) FROM keluarga k WHERE k.rumah_ibadah_id=ri.id AND k.status_ekonomi='miskin') as kk_miskin
|
||||
FROM rumah_ibadah ri ORDER BY ri.nama")->fetchAll();
|
||||
foreach ($rows as &$r) { $r['id']=(int)$r['id']; $r['lat']=(float)$r['lat']; $r['lng']=(float)$r['lng']; }
|
||||
jsonOk($rows); break;
|
||||
|
||||
case 'POST':
|
||||
requireRole(['superadmin']);
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$nama = sanitize($b['nama'] ?? '');
|
||||
if (!$nama) jsonErr('Nama wajib diisi.');
|
||||
$db->prepare("INSERT INTO rumah_ibadah (nama,jenis,kontak,lat,lng,alamat,kelurahan) VALUES (?,?,?,?,?,?,?)")
|
||||
->execute([$nama,$b['jenis']??'Masjid',sanitize($b['kontak']??''),(float)($b['lat']??0),(float)($b['lng']??0),sanitize($b['alamat']??''),sanitize($b['kelurahan']??'')]);
|
||||
jsonOk(['id'=>(int)$db->lastInsertId()],'Rumah ibadah ditambahkan.');
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireRole(['superadmin','pengurus']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$db->prepare("UPDATE rumah_ibadah SET nama=?,jenis=?,kontak=?,lat=?,lng=?,alamat=?,kelurahan=?,updated_at=NOW() WHERE id=?")
|
||||
->execute([sanitize($b['nama']??''),$b['jenis']??'Masjid',sanitize($b['kontak']??''),(float)($b['lat']??0),(float)($b['lng']??0),sanitize($b['alamat']??''),sanitize($b['kelurahan']??''),$id]);
|
||||
jsonOk(null,'Data diperbarui.');
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
requireRole(['superadmin']);
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$db->prepare("DELETE FROM rumah_ibadah WHERE id=?")->execute([$id]);
|
||||
jsonOk(null,'Data dihapus.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
requireLogin();
|
||||
$db = getDB();
|
||||
|
||||
// KPI Utama
|
||||
$totalPenduduk = (int)$db->query("SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup'")->fetchColumn();
|
||||
$totalKeluarga = (int)$db->query("SELECT COUNT(*) FROM keluarga")->fetchColumn();
|
||||
$totalMiskin = (int)$db->query("SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup'")->fetchColumn();
|
||||
$totalRentan = (int)$db->query("SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup'")->fetchColumn();
|
||||
$totalMampu = (int)$db->query("SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='mampu' AND status_hidup='hidup'")->fetchColumn();
|
||||
$totalLaporan = (int)$db->query("SELECT COUNT(*) FROM laporan WHERE status != 'selesai'")->fetchColumn();
|
||||
$laporanDarurat= (int)$db->query("SELECT COUNT(*) FROM laporan WHERE urgensi='darurat' AND status NOT IN ('selesai')")->fetchColumn();
|
||||
$totalBantuan = (int)$db->query("SELECT COUNT(*) FROM histori_bantuan WHERE status='disalurkan'")->fetchColumn();
|
||||
$belumBantuan = (int)$db->query("SELECT COUNT(*) FROM penduduk WHERE status_ekonomi IN ('miskin','rentan') AND status_hidup='hidup' AND id NOT IN (SELECT DISTINCT penduduk_id FROM histori_bantuan WHERE status='disalurkan')")->fetchColumn();
|
||||
$unreadChat = (int)$db->query("SELECT COUNT(*) FROM chat WHERE dibaca=0")->fetchColumn();
|
||||
|
||||
// Distribusi ekonomi per kelurahan
|
||||
$byKelurahan = $db->query("SELECT k.kelurahan,
|
||||
SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) as miskin,
|
||||
SUM(CASE WHEN k.status_ekonomi='rentan' THEN 1 ELSE 0 END) as rentan,
|
||||
SUM(CASE WHEN k.status_ekonomi='mampu' THEN 1 ELSE 0 END) as mampu,
|
||||
COUNT(*) as total
|
||||
FROM keluarga k GROUP BY k.kelurahan ORDER BY miskin DESC LIMIT 10")->fetchAll();
|
||||
|
||||
// Distribusi bantuan per jenis
|
||||
$byBantuan = $db->query("SELECT b.nama, b.jenis, COUNT(hb.id) as total_disalurkan
|
||||
FROM bantuan b LEFT JOIN histori_bantuan hb ON hb.bantuan_id=b.id AND hb.status='disalurkan'
|
||||
GROUP BY b.id ORDER BY total_disalurkan DESC")->fetchAll();
|
||||
|
||||
// Distribusi per rumah ibadah
|
||||
$byRI = $db->query("SELECT ri.nama, ri.jenis, ri.lat, ri.lng,
|
||||
COUNT(DISTINCT k.id) as jumlah_kk,
|
||||
SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) as kk_miskin,
|
||||
(SELECT COUNT(*) FROM v_penduduk p JOIN keluarga k2 ON k2.id=p.keluarga_id WHERE k2.rumah_ibadah_id=ri.id AND p.status_hidup='hidup') as jumlah_jiwa
|
||||
FROM rumah_ibadah ri LEFT JOIN keluarga k ON k.rumah_ibadah_id=ri.id
|
||||
GROUP BY ri.id ORDER BY kk_miskin DESC")->fetchAll();
|
||||
|
||||
// Profil umur
|
||||
$byUmur = $db->query("SELECT profil_umur, COUNT(*) as jumlah
|
||||
FROM v_penduduk WHERE status_hidup='hidup' GROUP BY profil_umur")->fetchAll();
|
||||
|
||||
// Laporan per status
|
||||
$byLaporan = $db->query("SELECT status, urgensi, COUNT(*) as jumlah
|
||||
FROM laporan GROUP BY status, urgensi ORDER BY FIELD(urgensi,'darurat','tinggi','sedang','rendah')")->fetchAll();
|
||||
|
||||
// GeoJSON penduduk untuk peta
|
||||
$geoRows = $db->query("SELECT p.id, p.nik, p.nama, p.umur, p.profil_umur, p.status_ekonomi,
|
||||
p.kondisi_kesehatan, p.penghasilan, k.kelurahan, k.lat, k.lng, k.no_kk,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.penduduk_id=p.id AND hb.status='disalurkan') as total_bantuan
|
||||
FROM v_penduduk p JOIN keluarga k ON k.id=p.keluarga_id
|
||||
WHERE p.status_hidup='hidup' AND k.lat IS NOT NULL AND k.lng IS NOT NULL
|
||||
LIMIT 1000")->fetchAll();
|
||||
|
||||
$features = [];
|
||||
foreach ($geoRows as $r) {
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['lng'],(float)$r['lat']]],
|
||||
'properties' => $r
|
||||
];
|
||||
}
|
||||
|
||||
// GeoJSON laporan untuk peta
|
||||
$laporanRows = $db->query("SELECT id,deskripsi,lat,lng,urgensi,status,created_at,foto FROM laporan WHERE lat IS NOT NULL AND lng IS NOT NULL")->fetchAll();
|
||||
$laporanFeatures = [];
|
||||
foreach ($laporanRows as $r) {
|
||||
$laporanFeatures[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['lng'],(float)$r['lat']]],
|
||||
'properties' => $r
|
||||
];
|
||||
}
|
||||
|
||||
jsonOk([
|
||||
'kpi' => [
|
||||
'total_penduduk' => $totalPenduduk,
|
||||
'total_keluarga' => $totalKeluarga,
|
||||
'total_miskin' => $totalMiskin,
|
||||
'total_rentan' => $totalRentan,
|
||||
'total_mampu' => $totalMampu,
|
||||
'total_laporan_aktif' => $totalLaporan,
|
||||
'laporan_darurat' => $laporanDarurat,
|
||||
'total_bantuan_disalurkan' => $totalBantuan,
|
||||
'belum_dapat_bantuan' => $belumBantuan,
|
||||
'unread_chat' => $unreadChat,
|
||||
'pct_miskin' => $totalPenduduk ? round($totalMiskin/$totalPenduduk*100,1) : 0,
|
||||
],
|
||||
'by_kelurahan' => $byKelurahan,
|
||||
'by_bantuan' => $byBantuan,
|
||||
'by_ri' => $byRI,
|
||||
'by_umur' => $byUmur,
|
||||
'by_laporan' => $byLaporan,
|
||||
'geojson_penduduk' => ['type'=>'FeatureCollection','features'=>$features],
|
||||
'geojson_laporan' => ['type'=>'FeatureCollection','features'=>$laporanFeatures],
|
||||
]);
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
// api/user.php — Manajemen User (superadmin only)
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$user = requireRole(['superadmin']);
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
$st = $db->prepare("SELECT u.id, u.username, u.nama_lengkap, u.telepon, u.aktif,
|
||||
r.kode as role, r.nama as role_nama, r.id as role_id,
|
||||
ri.nama as ri_nama, u.rumah_ibadah_id, u.created_at
|
||||
FROM user u JOIN role r ON r.id=u.role_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id
|
||||
WHERE u.id=?");
|
||||
$st->execute([$id]);
|
||||
jsonOk($st->fetch());
|
||||
}
|
||||
$rows = $db->query("SELECT u.id, u.username, u.nama_lengkap, u.telepon, u.aktif,
|
||||
r.kode as role, r.nama as role_nama, ri.nama as ri_nama, u.created_at
|
||||
FROM user u JOIN role r ON r.id=u.role_id
|
||||
LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id
|
||||
ORDER BY r.id, u.nama_lengkap")->fetchAll();
|
||||
jsonOk($rows);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$username = sanitize($b['username'] ?? '');
|
||||
$password = $b['password'] ?? '';
|
||||
$nama = sanitize($b['nama_lengkap'] ?? '');
|
||||
$roleId = (int)($b['role_id'] ?? 5);
|
||||
|
||||
if (!$username || !$password || !$nama) jsonErr('Username, password, dan nama wajib diisi.');
|
||||
if (strlen($username) < 4) jsonErr('Username minimal 4 karakter.');
|
||||
if (strlen($password) < 6) jsonErr('Password minimal 6 karakter.');
|
||||
|
||||
$chk = $db->prepare("SELECT id FROM user WHERE username=?"); $chk->execute([$username]);
|
||||
if ($chk->fetch()) jsonErr('Username sudah digunakan.');
|
||||
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
$db->prepare("INSERT INTO user (username, password, nama_lengkap, role_id, rumah_ibadah_id, telepon)
|
||||
VALUES (?,?,?,?,?,?)")
|
||||
->execute([$username, $hash, $nama, $roleId,
|
||||
!empty($b['rumah_ibadah_id']) ? (int)$b['rumah_ibadah_id'] : null,
|
||||
sanitize($b['telepon'] ?? '')]);
|
||||
$newId = (int)$db->lastInsertId();
|
||||
log_aksi($user['id'], 'CREATE', 'user', $newId, "Tambah user: $username");
|
||||
jsonOk(['id' => $newId], 'User berhasil ditambahkan.');
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
$b = json_decode(file_get_contents('php://input'), true);
|
||||
$nama = sanitize($b['nama_lengkap'] ?? '');
|
||||
if (!$nama) jsonErr('Nama wajib diisi.');
|
||||
|
||||
$chk = $db->prepare("SELECT id FROM user WHERE id=?"); $chk->execute([$id]);
|
||||
if (!$chk->fetch()) jsonErr('User tidak ditemukan.', 404);
|
||||
|
||||
// Update data dasar
|
||||
$db->prepare("UPDATE user SET nama_lengkap=?, role_id=?, rumah_ibadah_id=?, telepon=?, aktif=? WHERE id=?")
|
||||
->execute([$nama, (int)($b['role_id'] ?? 5),
|
||||
!empty($b['rumah_ibadah_id']) ? (int)$b['rumah_ibadah_id'] : null,
|
||||
sanitize($b['telepon'] ?? ''), (int)($b['aktif'] ?? 1), $id]);
|
||||
|
||||
// Ganti password jika diisi
|
||||
if (!empty($b['password']) && strlen($b['password']) >= 6) {
|
||||
$db->prepare("UPDATE user SET password=? WHERE id=?")
|
||||
->execute([password_hash($b['password'], PASSWORD_BCRYPT), $id]);
|
||||
}
|
||||
log_aksi($user['id'], 'UPDATE', 'user', $id, "Edit user ID: $id");
|
||||
jsonOk(null, 'Data user diperbarui.');
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
if (!$id) jsonErr('ID tidak valid.');
|
||||
if ($id === $user['id']) jsonErr('Tidak dapat menghapus akun sendiri.');
|
||||
$db->prepare("DELETE FROM user WHERE id=?")->execute([$id]);
|
||||
log_aksi($user['id'], 'DELETE', 'user', $id, "Hapus user ID: $id");
|
||||
jsonOk(null, 'User dihapus.');
|
||||
break;
|
||||
|
||||
default: jsonErr('Method tidak didukung.', 405);
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config.php — WebGIS Kemiskinan v3.0
|
||||
// ============================================================
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_kemiskinan');
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
|
||||
date_default_timezone_set('Asia/Pontianak');
|
||||
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4", DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
jsonErr('Koneksi database gagal: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function startSession(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name('webgis_v3');
|
||||
session_set_cookie_params(['lifetime'=>28800,'path'=>'/','httponly'=>true,'samesite'=>'Strict']);
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentUser(): ?array {
|
||||
startSession();
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
function requireLogin(): array {
|
||||
$u = getCurrentUser();
|
||||
if (!$u) jsonErr('Silakan login terlebih dahulu.', 401);
|
||||
return $u;
|
||||
}
|
||||
|
||||
function requireRole(array $roles): array {
|
||||
$u = requireLogin();
|
||||
if (!in_array($u['role'], $roles, true)) jsonErr('Akses ditolak untuk role: '.$u['role'], 403);
|
||||
return $u;
|
||||
}
|
||||
|
||||
function canWrite(): bool {
|
||||
$u = getCurrentUser();
|
||||
return $u && in_array($u['role'], ['superadmin','pengurus','relawan'], true);
|
||||
}
|
||||
|
||||
function jsonOk($data = null, string $msg = 'Berhasil'): void {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success'=>true,'message'=>$msg,'data'=>$data], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function jsonErr(string $msg, int $code = 400): void {
|
||||
http_response_code($code);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success'=>false,'message'=>$msg], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function log_aksi(int $userId, string $aksi, string $tabel, int $recId, string $ket = ''): void {
|
||||
try {
|
||||
getDB()->prepare("INSERT INTO log_aktivitas (user_id,aksi,tabel,record_id,keterangan,ip_address) VALUES (?,?,?,?,?,?)")
|
||||
->execute([$userId, $aksi, $tabel, $recId, $ket, $_SERVER['REMOTE_ADDR'] ?? '']);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
function sanitize(string $s): string {
|
||||
return htmlspecialchars(strip_tags(trim($s)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
// Hitung umur dari tanggal lahir
|
||||
function hitungUmur(string $tgl): int {
|
||||
return (int) date_diff(date_create($tgl), date_create('today'))->y;
|
||||
}
|
||||
|
||||
// Validasi pendidikan vs umur
|
||||
function validasiPendidikan(int $umur, string $pendidikan, string $statusPendidikan): ?string {
|
||||
if ($umur >= 7 && $umur <= 15 && $statusPendidikan === 'tidak_sekolah'
|
||||
&& in_array($pendidikan, ['tidak_sekolah','sd'])) {
|
||||
return 'Peringatan: Usia 7-15 tahun seharusnya masih sekolah SD/SMP.';
|
||||
}
|
||||
if ($umur >= 16 && $umur <= 18 && $statusPendidikan === 'tidak_sekolah') {
|
||||
return 'Peringatan: Usia 16-18 tahun seharusnya masih sekolah SMA/SMK.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
-- ================================================================
|
||||
-- WebGIS Pengentasan Kemiskinan — Kota Pontianak v3.0
|
||||
-- Database Lengkap: 13 tabel, 5 role, relasi kuat
|
||||
-- Jalankan di phpMyAdmin > tab SQL > klik Go
|
||||
-- ================================================================
|
||||
|
||||
DROP DATABASE IF EXISTS webgis_kemiskinan;
|
||||
CREATE DATABASE webgis_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis_kemiskinan;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 1. ROLE
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE role (
|
||||
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
kode VARCHAR(30) NOT NULL UNIQUE,
|
||||
nama VARCHAR(80) NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO role (kode, nama) VALUES
|
||||
('superadmin', 'Super Admin'),
|
||||
('pimpinan', 'Pimpinan Daerah / Pemerintah'),
|
||||
('pengurus', 'Pengurus Rumah Ibadah'),
|
||||
('relawan', 'Relawan Lapangan'),
|
||||
('masyarakat', 'Masyarakat Umum');
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 2. RUMAH IBADAH
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE rumah_ibadah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(200) NOT NULL,
|
||||
jenis ENUM('Masjid','Gereja','Vihara','Klenteng','Pura') NOT NULL,
|
||||
kontak VARCHAR(60),
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
alamat TEXT,
|
||||
kelurahan VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 3. USER
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE user (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(80) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
nama_lengkap VARCHAR(150) NOT NULL,
|
||||
role_id TINYINT UNSIGNED NOT NULL,
|
||||
rumah_ibadah_id INT DEFAULT NULL,
|
||||
telepon VARCHAR(20),
|
||||
aktif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES role(id),
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 4. KELUARGA (KK)
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE keluarga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
no_kk VARCHAR(20) NOT NULL UNIQUE,
|
||||
nama_kk VARCHAR(200) NOT NULL, -- nama kepala keluarga
|
||||
alamat TEXT,
|
||||
rt VARCHAR(5),
|
||||
rw VARCHAR(5),
|
||||
kelurahan VARCHAR(100),
|
||||
kecamatan VARCHAR(100),
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
rumah_ibadah_id INT DEFAULT NULL,
|
||||
status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
|
||||
created_by INT,
|
||||
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 (created_by) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 5. PENDUDUK
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE penduduk (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nik VARCHAR(16) NOT NULL UNIQUE,
|
||||
nama VARCHAR(200) NOT NULL,
|
||||
jenis_kelamin ENUM('L','P') NOT NULL,
|
||||
tanggal_lahir DATE NOT NULL,
|
||||
-- umur & profil_umur dihitung via VIEW (kompatibel semua versi MySQL)
|
||||
-- kolom ini diisi/diperbarui otomatis oleh PHP saat INSERT/UPDATE
|
||||
status_keluarga ENUM('kepala','istri','anak','orang_tua','lainnya') NOT NULL DEFAULT 'kepala',
|
||||
status_perkawinan ENUM('belum_kawin','kawin','cerai_hidup','cerai_mati') NOT NULL DEFAULT 'belum_kawin',
|
||||
status_hidup ENUM('hidup','meninggal') NOT NULL DEFAULT 'hidup',
|
||||
keluarga_id INT NOT NULL,
|
||||
-- pendidikan
|
||||
pendidikan_terakhir ENUM('tidak_sekolah','sd','smp','sma','smk','d3','s1','s2','s3') DEFAULT 'tidak_sekolah',
|
||||
status_pendidikan ENUM('masih_sekolah','tidak_sekolah') DEFAULT 'tidak_sekolah',
|
||||
-- ekonomi
|
||||
pekerjaan VARCHAR(100),
|
||||
penghasilan DECIMAL(12,2) DEFAULT 0,
|
||||
status_ekonomi ENUM('miskin','rentan','mampu') NOT NULL DEFAULT 'rentan',
|
||||
-- kondisi kesehatan
|
||||
kondisi_kesehatan ENUM('sehat','sakit_ringan','sakit_berat','disabilitas') DEFAULT 'sehat',
|
||||
-- lokasi (bisa berbeda dari KK jika merantau)
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
-- meta
|
||||
foto VARCHAR(255),
|
||||
catatan TEXT,
|
||||
created_by INT,
|
||||
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 CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 6. PELATIHAN
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE pelatihan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_pelatihan VARCHAR(200) NOT NULL,
|
||||
penyelenggara VARCHAR(150),
|
||||
tanggal DATE,
|
||||
durasi_hari TINYINT UNSIGNED DEFAULT 1,
|
||||
deskripsi TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE riwayat_pelatihan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
pelatihan_id INT NOT NULL,
|
||||
tanggal_ikut DATE,
|
||||
sertifikat VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (pelatihan_id) REFERENCES pelatihan(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 7. BANTUAN
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(200) NOT NULL,
|
||||
jenis ENUM('sembako','uang_tunai','kesehatan','pendidikan','perumahan','lainnya') NOT NULL,
|
||||
sumber VARCHAR(150), -- PKH, BPNT, Baznas, Dinsos, dll
|
||||
satuan VARCHAR(50), -- karung, paket, Rp, dll
|
||||
nilai_per_satuan DECIMAL(12,2) DEFAULT 0,
|
||||
periode ENUM('sekali','bulanan','triwulan','tahunan') DEFAULT 'sekali',
|
||||
aktif TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE histori_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
bantuan_id INT NOT NULL,
|
||||
disalurkan_oleh INT, -- user_id penyalur
|
||||
tanggal DATE NOT NULL,
|
||||
jumlah DECIMAL(10,2) DEFAULT 1,
|
||||
status ENUM('dijadwalkan','disalurkan','gagal') DEFAULT 'dijadwalkan',
|
||||
catatan TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (bantuan_id) REFERENCES bantuan(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (disalurkan_oleh) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 8. LAPORAN MASYARAKAT
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE laporan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
pelapor_nama VARCHAR(150), -- bisa anonim
|
||||
pelapor_telepon VARCHAR(20),
|
||||
penduduk_id INT DEFAULT NULL, -- jika tahu NIK
|
||||
deskripsi TEXT NOT NULL,
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
alamat_laporan TEXT,
|
||||
foto VARCHAR(255),
|
||||
urgensi ENUM('rendah','sedang','tinggi','darurat') DEFAULT 'sedang',
|
||||
status ENUM('pending','diverifikasi','diproses','selesai') DEFAULT 'pending',
|
||||
diverifikasi_oleh INT DEFAULT NULL,
|
||||
catatan_admin 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 SET NULL,
|
||||
FOREIGN KEY (diverifikasi_oleh) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 9. CHAT
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE chat (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
dari_user_id INT NOT NULL,
|
||||
ke_user_id INT DEFAULT NULL, -- NULL = broadcast ke semua pengurus
|
||||
pesan TEXT NOT NULL,
|
||||
dibaca TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (dari_user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ke_user_id) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- 10. LOG AKTIVITAS
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE TABLE log_aktivitas (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
aksi VARCHAR(60) NOT NULL,
|
||||
tabel VARCHAR(60),
|
||||
record_id INT,
|
||||
keterangan TEXT,
|
||||
ip_address VARCHAR(45),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- VIEW: Hitung umur & profil_umur otomatis
|
||||
-- Kompatibel semua versi MySQL/MariaDB
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW v_penduduk AS
|
||||
SELECT
|
||||
p.*,
|
||||
TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) AS umur,
|
||||
CASE
|
||||
WHEN TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) < 5 THEN 'Balita'
|
||||
WHEN TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) < 12 THEN 'Anak'
|
||||
WHEN TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) < 18 THEN 'Remaja'
|
||||
WHEN TIMESTAMPDIFF(YEAR, p.tanggal_lahir, CURDATE()) < 60 THEN 'Dewasa'
|
||||
ELSE 'Lansia'
|
||||
END AS profil_umur
|
||||
FROM penduduk p;
|
||||
|
||||
-- ────────────────────────────────────────────────
|
||||
-- INDEX PERFORMA
|
||||
-- ────────────────────────────────────────────────
|
||||
CREATE INDEX idx_penduduk_nik ON penduduk(nik);
|
||||
CREATE INDEX idx_penduduk_keluarga ON penduduk(keluarga_id);
|
||||
CREATE INDEX idx_penduduk_ekonomi ON penduduk(status_ekonomi);
|
||||
CREATE INDEX idx_penduduk_hidup ON penduduk(status_hidup);
|
||||
CREATE INDEX idx_keluarga_ekonomi ON keluarga(status_ekonomi);
|
||||
CREATE INDEX idx_keluarga_ri ON keluarga(rumah_ibadah_id);
|
||||
CREATE INDEX idx_histori_penduduk ON histori_bantuan(penduduk_id);
|
||||
CREATE INDEX idx_histori_tgl ON histori_bantuan(tanggal);
|
||||
CREATE INDEX idx_laporan_status ON laporan(status);
|
||||
CREATE INDEX idx_laporan_urgensi ON laporan(urgensi);
|
||||
CREATE INDEX idx_chat_ke ON chat(ke_user_id);
|
||||
CREATE INDEX idx_log_user ON log_aktivitas(user_id);
|
||||
|
||||
-- ================================================================
|
||||
-- DATA AWAL
|
||||
-- ================================================================
|
||||
|
||||
-- RUMAH IBADAH
|
||||
INSERT INTO rumah_ibadah (nama, jenis, kontak, lat, lng, alamat, kelurahan) VALUES
|
||||
('Masjid Raya Mujahidin', 'Masjid', '0561-731234', -0.0193000, 109.3421000, 'Jl. Ahmad Yani No.1', 'Akcaya'),
|
||||
('Gereja Katedral St. Yosef', 'Gereja', '0561-734567', -0.0298000, 109.3334000, 'Jl. Pattimura No.8', 'Tengah'),
|
||||
('Masjid Al-Falah', 'Masjid', '0561-762111', -0.0089000, 109.3508000, 'Jl. Sultan Syarif No.3', 'Batu Layang'),
|
||||
('Vihara Bodhisattva Karaniya', 'Vihara', '0561-748999', -0.0351000, 109.3287000, 'Jl. Diponegoro No.12', 'Parit Tokaya'),
|
||||
('Gereja Bethel Indonesia', 'Gereja', '0561-712888', -0.0431000, 109.3376000, 'Jl. Imam Bonjol No.5', 'Benua Melayu Darat'),
|
||||
('Masjid Jami Sultan Syarif', 'Masjid', '0561-721000', -0.0145000, 109.3289000, 'Jl. Tanjung Raya II No.2', 'Saigon'),
|
||||
('Klenteng Kwan Tie Miau', 'Klenteng','0561-737000', -0.0265000, 109.3301000, 'Jl. Diponegoro No.3', 'Mariana');
|
||||
|
||||
-- USER (password = "password" plaintext, bisa di-hash bcrypt via PHP)
|
||||
INSERT INTO user (username, password, nama_lengkap, role_id, rumah_ibadah_id, telepon) VALUES
|
||||
('superadmin', 'password', 'Super Administrator', 1, NULL, '08100000001'),
|
||||
('pimpinan1', 'password', 'H. Andi Rachman (Walikota)', 2, NULL, '08100000002'),
|
||||
('pengurus1', 'password', 'Ustadz H. Mahmud (Mujahidin)',3, 1, '08100000003'),
|
||||
('pengurus2', 'password', 'Rm. Petrus Lioe (Katedral)', 3, 2, '08100000004'),
|
||||
('pengurus3', 'password', 'Pak Budi (Al-Falah)', 3, 3, '08100000005'),
|
||||
('relawan1', 'password', 'Sri Wahyuni (Relawan Utara)', 4, NULL, '08100000006'),
|
||||
('relawan2', 'password', 'Doni Pratama (Relawan Selatan)',4,NULL, '08100000007'),
|
||||
('masyarakat1', 'password', 'Warga Umum 1', 5, NULL, '08100000008');
|
||||
|
||||
-- KELUARGA
|
||||
INSERT INTO keluarga (no_kk, nama_kk, alamat, rt, rw, kelurahan, kecamatan, lat, lng, rumah_ibadah_id, status_ekonomi, created_by) VALUES
|
||||
('6171011234560001','Suharto', 'Jl. Tanjung Raya I No.12', '03','05','Saigon', 'Pontianak Timur', -0.0211000, 109.3445000, 1, 'miskin', 1),
|
||||
('6171011234560002','Ahmad Fauzi', 'Jl. Parit Makmur No.7', '01','02','Batu Layang', 'Pontianak Utara', -0.0178000, 109.3398000, 3, 'miskin', 1),
|
||||
('6171011234560003','Yulianus Sari', 'Jl. Veteran No.3', '02','01','Tengah', 'Pontianak Kota', -0.0323000, 109.3312000, 2, 'rentan', 1),
|
||||
('6171011234560004','Robert Tanto', 'Jl. Gajahmada No.21', '04','03','Mariana', 'Pontianak Kota', -0.0289000, 109.3356000, 7, 'rentan', 1),
|
||||
('6171011234560005','Samsul Bahri', 'Jl. Khatulistiwa No.5', '01','01','Batu Layang', 'Pontianak Utara', -0.0072000, 109.3521000, 3, 'miskin', 1),
|
||||
('6171011234560006','Dewi Rahmawati', 'Jl. Sultan Hamid II No.8','02','04','Benua Melayu Darat', 'Pontianak Selatan',-0.0431000, 109.3401000, 5, 'miskin', 1),
|
||||
('6171011234560007','Suparjo', 'Jl. Budi Utomo No.3', '05','02','Saigon', 'Pontianak Timur', -0.0148000, 109.3467000, 6, 'rentan', 1),
|
||||
('6171011234560008','Maria Goreti Ngo','Jl. Sutoyo No.15', '03','01','Parit Tokaya', 'Pontianak Selatan',-0.0372000, 109.3271000, 4, 'miskin', 1),
|
||||
('6171011234560009','Lim Beng Huat', 'Jl. Diponegoro No.4', '01','01','Mariana', 'Pontianak Kota', -0.0340000, 109.3290000, 7, 'rentan', 1),
|
||||
('6171011234560010','Daud Kristianto', 'Jl. Hos Cokroaminoto No.9','04','02','Akcaya', 'Pontianak Selatan',-0.0198000, 109.3372000, 1, 'miskin', 1);
|
||||
|
||||
-- PENDUDUK (contoh lengkap dengan NIK, umur, dll)
|
||||
INSERT INTO penduduk (nik, nama, jenis_kelamin, tanggal_lahir, status_keluarga, status_perkawinan, keluarga_id, pendidikan_terakhir, status_pendidikan, pekerjaan, penghasilan, status_ekonomi, kondisi_kesehatan, lat, lng, created_by) VALUES
|
||||
('6171011234560001','Suharto', 'L','1975-03-12','kepala','kawin', 1,'sd', 'tidak_sekolah','Buruh Harian', 500000, 'miskin', 'sehat', -0.0211000,109.3445000,1),
|
||||
('6171011234560002','Sri Mulyani', 'P','1978-07-20','istri', 'kawin', 1,'smp', 'tidak_sekolah','Ibu Rumah Tangga',0, 'miskin', 'sakit_ringan', -0.0211000,109.3445000,1),
|
||||
('6171011234560003','Budi Suharto', 'L','2008-01-15','anak', 'belum_kawin', 1,'smp', 'masih_sekolah', NULL, 0, 'miskin', 'sehat', -0.0211000,109.3445000,1),
|
||||
('6171011234560004','Ahmad Fauzi', 'L','1968-11-05','kepala','kawin', 2,'sd', 'tidak_sekolah','Nelayan', 800000,'miskin', 'sehat', -0.0178000,109.3398000,1),
|
||||
('6171011234560005','Siti Aminah', 'P','1972-04-17','istri', 'kawin', 2,'sd', 'tidak_sekolah','Pedagang Kecil',300000,'miskin', 'sehat', -0.0178000,109.3398000,1),
|
||||
('6171011234560006','Yulianus Sari', 'L','1980-09-22','kepala','kawin', 3,'sma', 'tidak_sekolah','Ojek Online', 1200000,'rentan','sehat', -0.0323000,109.3312000,1),
|
||||
('6171011234560007','Robert Tanto', 'L','1985-06-18','kepala','kawin', 4,'sma', 'tidak_sekolah','Karyawan Swasta',2000000,'rentan','sehat', -0.0289000,109.3356000,1),
|
||||
('6171011234560008','Samsul Bahri', 'L','1958-12-30','kepala','kawin', 5,'sd', 'tidak_sekolah','Petani', 400000,'miskin','sakit_ringan', -0.0072000,109.3521000,1),
|
||||
('6171011234560009','Dewi Rahmawati', 'P','1990-02-14','kepala','cerai_mati', 6,'smp', 'tidak_sekolah','Buruh Cuci', 600000,'miskin','sehat', -0.0431000,109.3401000,1),
|
||||
('6171011234560010','Nur Azizah', 'P','2010-05-10','anak', 'belum_kawin', 6,'sd', 'masih_sekolah', NULL, 0, 'miskin', 'sehat', -0.0431000,109.3401000,1),
|
||||
('6171011234560011','Suparjo', 'L','1970-08-25','kepala','kawin', 7,'sd', 'tidak_sekolah','Kuli Bangunan', 700000,'rentan','sehat', -0.0148000,109.3467000,1),
|
||||
('6171011234560012','Maria Goreti', 'P','1963-10-08','kepala','cerai_mati', 8,'sd', 'tidak_sekolah','Penjual Sayur', 450000,'miskin','disabilitas', -0.0372000,109.3271000,1),
|
||||
('6171011234560013','Lim Beng Huat', 'L','1955-03-17','kepala','kawin', 9,'smp', 'tidak_sekolah','Pensiunan', 800000,'rentan','sakit_ringan', -0.0340000,109.3290000,1),
|
||||
('6171011234560014','Daud Kristianto', 'L','1972-07-04','kepala','kawin', 10,'sma', 'tidak_sekolah','Pedagang', 900000,'miskin','sehat', -0.0198000,109.3372000,1),
|
||||
('6171011234560015','Yanto Pramono', 'L','2007-09-12','anak', 'belum_kawin', 7,'smp', 'masih_sekolah', NULL, 0, 'rentan','sehat', -0.0148000,109.3467000,1);
|
||||
|
||||
-- PELATIHAN
|
||||
INSERT INTO pelatihan (nama_pelatihan, penyelenggara, tanggal, durasi_hari, deskripsi) VALUES
|
||||
('Pelatihan Menjahit Dasar', 'Dinsos Pontianak', '2024-03-15', 7, 'Pelatihan menjahit untuk ibu rumah tangga'),
|
||||
('Kursus Memasak & Katering', 'Baznas Kalbar', '2024-04-20', 5, 'Pelatihan memasak dan usaha katering'),
|
||||
('Budidaya Sayuran Hidroponik', 'Dinas Pertanian', '2024-05-10', 3, 'Teknik menanam sayuran di lahan terbatas'),
|
||||
('Keterampilan Las Listrik', 'BLK Pontianak', '2024-06-05', 14, 'Keterampilan las untuk remaja putus sekolah'),
|
||||
('Digital Marketing UMKM', 'Kominfo Pontianak', '2024-07-01', 2, 'Pemasaran online untuk usaha mikro');
|
||||
|
||||
-- RIWAYAT PELATIHAN
|
||||
INSERT INTO riwayat_pelatihan (penduduk_id, pelatihan_id, tanggal_ikut) VALUES
|
||||
(6, 1, '2024-03-15'), (9, 1, '2024-03-15'),
|
||||
(7, 2, '2024-04-20'), (12, 3, '2024-05-10');
|
||||
|
||||
-- BANTUAN
|
||||
INSERT INTO bantuan (nama, jenis, sumber, satuan, nilai_per_satuan, periode) VALUES
|
||||
('PKH (Program Keluarga Harapan)', 'uang_tunai', 'Kemensos RI', 'Rp', 750000, 'triwulan'),
|
||||
('BPNT (Sembako)', 'sembako', 'Kemensos RI', 'paket', 200000, 'bulanan'),
|
||||
('BLT Dana Desa', 'uang_tunai', 'APBDes', 'Rp', 300000, 'bulanan'),
|
||||
('Bantuan Baznas Kalbar', 'sembako', 'Baznas Kalbar', 'paket', 150000, 'bulanan'),
|
||||
('JKN PBI (BPJS Gratis)', 'kesehatan', 'APBN/APBD', 'kartu', 0, 'tahunan'),
|
||||
('Beasiswa Anak Miskin', 'pendidikan', 'Pemkot Pontianak', 'Rp', 500000, 'triwulan'),
|
||||
('Bantuan Sembako Masjid Mujahidin','sembako', 'ZIS Mujahidin', 'paket', 100000, 'bulanan'),
|
||||
('Program Bedah Rumah', 'perumahan', 'APBD Kota', 'unit', 15000000,'sekali');
|
||||
|
||||
-- HISTORI BANTUAN
|
||||
INSERT INTO histori_bantuan (penduduk_id, bantuan_id, disalurkan_oleh, tanggal, jumlah, status, catatan) VALUES
|
||||
(4, 1, 3, '2024-01-15', 1, 'disalurkan', 'PKH triwulan 1 2024'),
|
||||
(4, 2, 3, '2024-01-20', 1, 'disalurkan', 'BPNT Januari'),
|
||||
(4, 2, 3, '2024-02-20', 1, 'disalurkan', 'BPNT Februari'),
|
||||
(9, 1, 4, '2024-01-15', 1, 'disalurkan', 'PKH triwulan 1 2024'),
|
||||
(9, 2, 4, '2024-01-22', 1, 'disalurkan', 'BPNT Januari'),
|
||||
(1, 4, 3, '2024-02-01', 1, 'disalurkan', 'Bantuan Baznas Februari'),
|
||||
(8, 4, 3, '2024-02-01', 1, 'disalurkan', 'Bantuan Baznas Februari'),
|
||||
(12, 7, 4, '2024-02-05', 1, 'disalurkan', 'Sembako Katedral Februari'),
|
||||
(14, 3, 5, '2024-02-10', 1, 'disalurkan', 'BLT Dana Desa'),
|
||||
(7, 2, 3, '2024-03-20', 1, 'dijadwalkan','Dijadwalkan Maret');
|
||||
|
||||
-- LAPORAN MASYARAKAT
|
||||
INSERT INTO laporan (pelapor_nama, penduduk_id, deskripsi, lat, lng, alamat_laporan, urgensi, status) VALUES
|
||||
('Anonim', 8, 'Pak Samsul Bahri sakit keras sudah 2 minggu, tidak ada yang merawat dan tidak punya biaya berobat.', -0.0072000, 109.3521000, 'Jl. Khatulistiwa Pontianak Utara', 'darurat', 'diverifikasi'),
|
||||
('Ani Marlina', 12, 'Ibu Maria sudah tua dan mengalami cacat, tinggal sendirian dan kesulitan memenuhi kebutuhan sehari-hari.', -0.0372000, 109.3271000, 'Jl. Sutoyo Pontianak Selatan', 'tinggi', 'diproses'),
|
||||
('Warga RT 03', NULL, 'Ada keluarga baru pindah di Jl. Karet yang tampak sangat tidak mampu, rumah tidak layak huni.', -0.0195000, 109.3388000, 'Jl. Karet RT 03 RW 02', 'sedang', 'pending'),
|
||||
('Pak Hasan', 1, 'Keluarga Suharto belum dapat bantuan sama sekali padahal sangat membutuhkan.', -0.0211000, 109.3445000, 'Jl. Tanjung Raya I', 'tinggi', 'pending');
|
||||
|
||||
-- CHAT
|
||||
INSERT INTO chat (dari_user_id, ke_user_id, pesan) VALUES
|
||||
(3, NULL, 'Mohon perhatian semua pengurus: ada warga darurat di wilayah Utara, Pak Samsul Bahri butuh bantuan segera 🆘'),
|
||||
(4, 3, 'Siap, kami dari Katedral akan kirim relawan ke sana hari ini.'),
|
||||
(3, 4, 'Terima kasih Rm. Petrus. Koordinasi dengan Pak Budi dari Al-Falah ya karena lebih dekat.'),
|
||||
(5, NULL, 'Update: bantuan sembako sudah diserahkan ke Pak Samsul. Beliau butuh juga bantuan biaya berobat.'),
|
||||
(6, NULL, 'Laporan survey: 3 rumah tidak layak huni ditemukan di Jl. Karet. Foto sudah diupload.');
|
||||
|
||||
-- LOG AKTIVITAS
|
||||
INSERT INTO log_aktivitas (user_id, aksi, tabel, record_id, keterangan) VALUES
|
||||
(1, 'CREATE', 'keluarga', 1, 'Input keluarga baru: Suharto'),
|
||||
(1, 'CREATE', 'penduduk', 1, 'Input penduduk baru: NIK 6171011234560001'),
|
||||
(3, 'UPDATE', 'histori_bantuan',1, 'Catat penyaluran PKH ke Ahmad Fauzi'),
|
||||
(3, 'CREATE', 'laporan', 1, 'Laporan darurat: Samsul Bahri sakit keras'),
|
||||
(4, 'UPDATE', 'laporan', 2, 'Update status laporan Maria Goreti ke diproses'),
|
||||
(6, 'CREATE', 'laporan', 3, 'Survey lapangan: 3 rumah tidak layak di Jl. Karet');
|
||||
+767
@@ -0,0 +1,767 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>WebGIS Kemiskinan — Kota Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0a0d14;--bg2:#111520;--bg3:#181d2e;--bg4:#1e253a;--bg5:#252e45;
|
||||
--b:rgba(255,255,255,.06);--b2:rgba(255,255,255,.11);--b3:rgba(255,255,255,.17);
|
||||
--t:#dde2f0;--t2:#7e899e;--t3:#4a5368;
|
||||
--blue:#4f8ef7;--bdim:rgba(79,142,247,.14);--bglow:rgba(79,142,247,.28);
|
||||
--green:#34d399;--gdim:rgba(52,211,153,.13);
|
||||
--red:#f87171;--rdim:rgba(248,113,113,.13);
|
||||
--amber:#fbbf24;--adim:rgba(251,191,36,.13);
|
||||
--purple:#a78bfa;--pdim:rgba(167,139,250,.13);
|
||||
--rose:#fb7185;--rosedim:rgba(251,113,133,.13);
|
||||
--r:8px;--rs:6px;--rl:12px;
|
||||
--font:'Inter',sans-serif;--mono:'JetBrains Mono',monospace;
|
||||
--shadow:0 4px 24px rgba(0,0,0,.45);
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased}
|
||||
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t);font-size:13px;overflow:hidden}
|
||||
body{display:flex;flex-direction:column}
|
||||
::-webkit-scrollbar{width:4px;height:4px}
|
||||
::-webkit-scrollbar-thumb{background:var(--bg5);border-radius:4px}
|
||||
|
||||
/* LOGIN */
|
||||
#login-screen{position:fixed;inset:0;background:var(--bg);z-index:99999;display:flex;align-items:center;justify-content:center;background-image:radial-gradient(ellipse at 20% 50%,rgba(79,142,247,.06) 0%,transparent 60%),radial-gradient(ellipse at 80% 20%,rgba(167,139,250,.05) 0%,transparent 50%)}
|
||||
#login-screen.hidden{display:none}
|
||||
.lcard{background:var(--bg2);border:1px solid var(--b2);border-radius:16px;padding:40px 36px;width:420px;max-width:96vw;box-shadow:var(--shadow)}
|
||||
.lbrand{display:flex;align-items:center;gap:14px;margin-bottom:32px;padding-bottom:24px;border-bottom:1px solid var(--b)}
|
||||
.lbrand-icon{width:44px;height:44px;background:linear-gradient(135deg,rgba(79,142,247,.9),rgba(167,139,250,.9));border-radius:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.lbrand h1{font-size:15px;font-weight:700;letter-spacing:-.3px;line-height:1.2}
|
||||
.lbrand p{font-size:11px;color:var(--t3);margin-top:3px;font-family:var(--mono)}
|
||||
.lheading{font-size:22px;font-weight:700;letter-spacing:-.5px;margin-bottom:6px}
|
||||
.lsub{font-size:12px;color:var(--t3);margin-bottom:24px;line-height:1.6}
|
||||
.lerr{font-size:12px;color:var(--red);background:var(--rdim);border:1px solid rgba(248,113,113,.2);border-radius:var(--rs);padding:10px 13px;margin-bottom:16px;display:none;line-height:1.5}
|
||||
.lf{margin-bottom:16px}
|
||||
.lf label{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.7px;margin-bottom:7px}
|
||||
.lf input{width:100%;padding:11px 14px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:13px;outline:none;transition:all .18s}
|
||||
.lf input:focus{border-color:var(--blue);background:var(--bg4);box-shadow:0 0 0 3px var(--bdim)}
|
||||
.lf input::placeholder{color:var(--t3)}
|
||||
.lbtn{width:100%;height:46px;background:linear-gradient(135deg,var(--blue),#6366f1);border:none;border-radius:var(--rs);color:#fff;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;transition:all .18s;margin-top:8px;letter-spacing:-.1px;box-shadow:0 2px 12px rgba(79,142,247,.35)}
|
||||
.lbtn:hover{opacity:.92;transform:translateY(-1px)}
|
||||
.lbtn:disabled{background:var(--bg4);box-shadow:none;cursor:not-allowed;transform:none;color:var(--t3)}
|
||||
.laccs{margin-top:20px;padding:14px;background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs)}
|
||||
.laccs-title{font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.7px;margin-bottom:10px}
|
||||
.laccs table{width:100%;border-collapse:collapse;font-size:11px}
|
||||
.laccs td{padding:4px 0}
|
||||
.laccs td:first-child{color:var(--t2);font-weight:600;width:44%}
|
||||
.laccs td:last-child{font-family:var(--mono);color:var(--t3);font-size:10px}
|
||||
|
||||
/* TOPBAR */
|
||||
#topbar{height:52px;background:var(--bg2);border-bottom:1px solid var(--b);display:flex;align-items:center;padding:0 14px;gap:6px;flex-shrink:0;z-index:200}
|
||||
.brand{display:flex;align-items:center;gap:10px;margin-right:auto}
|
||||
.brand-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--blue),var(--purple));border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.brand h1{font-size:13px;font-weight:700;letter-spacing:-.3px}
|
||||
.brand p{font-size:10px;color:var(--t3);font-family:var(--mono)}
|
||||
.upill{display:flex;align-items:center;gap:7px;padding:5px 11px;background:var(--bg3);border:1px solid var(--b2);border-radius:20px}
|
||||
.upill .uname{font-size:11px;font-weight:600}
|
||||
.rbadge{font-size:9px;font-weight:700;padding:2px 8px;border-radius:20px;text-transform:uppercase;letter-spacing:.5px}
|
||||
.r-superadmin{background:var(--pdim);color:var(--purple)}
|
||||
.r-pimpinan{background:var(--bdim);color:var(--blue)}
|
||||
.r-pengurus{background:var(--gdim);color:var(--green)}
|
||||
.r-relawan{background:var(--adim);color:var(--amber)}
|
||||
.r-masyarakat{background:var(--b);color:var(--t2)}
|
||||
.nav-btn{height:32px;padding:0 12px;border-radius:var(--rs);border:1px solid transparent;background:transparent;color:var(--t2);font-family:var(--font);font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s;white-space:nowrap}
|
||||
.nav-btn:hover{background:var(--bg3);color:var(--t)}
|
||||
.nav-btn.active{background:var(--bdim);color:var(--blue);border-color:rgba(79,142,247,.3)}
|
||||
.ndot{width:6px;height:6px;border-radius:50%;background:var(--red);flex-shrink:0;animation:pulse 1.8s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.2}}
|
||||
.tsep{width:1px;height:22px;background:var(--b2);flex-shrink:0;margin:0 2px}
|
||||
|
||||
/* BUTTONS */
|
||||
.btn{height:32px;padding:0 13px;border-radius:var(--rs);border:1px solid var(--b2);background:var(--bg3);color:var(--t);font-family:var(--font);font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s;white-space:nowrap}
|
||||
.btn:hover{background:var(--bg4);border-color:var(--b3)}.btn:active{transform:scale(.97)}.btn:disabled{opacity:.3;cursor:not-allowed;pointer-events:none}
|
||||
.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff}.btn-primary:hover{background:#3b7cf0}
|
||||
.btn-success{background:var(--green);border-color:var(--green);color:#0a0d14;font-weight:600}.btn-success:hover{background:#2ec48e}
|
||||
.btn-danger{background:var(--red);border-color:var(--red);color:#fff}.btn-danger:hover{background:#f25c5c}
|
||||
.btn-ghost{background:transparent;border-color:transparent;color:var(--t2)}.btn-ghost:hover{background:var(--bg3);color:var(--t);border-color:var(--b)}
|
||||
.btn-sm{height:28px;padding:0 10px;font-size:11px;border-radius:5px}
|
||||
.btn-xs{height:24px;padding:0 8px;font-size:10px;border-radius:4px}
|
||||
|
||||
/* LAYOUT */
|
||||
#app{display:flex;flex:1;overflow:hidden}
|
||||
#sidebar{width:284px;min-width:284px;background:var(--bg2);border-right:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden;transition:width .25s,min-width .25s}
|
||||
#sidebar.off{width:0;min-width:0}
|
||||
.stabs{display:flex;border-bottom:1px solid var(--b);flex-shrink:0}
|
||||
.stab{flex:1;padding:10px 4px;text-align:center;cursor:pointer;font-size:9px;font-weight:700;color:var(--t3);border-bottom:2px solid transparent;transition:all .15s;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
|
||||
.stab:hover{color:var(--t2)}.stab.active{color:var(--blue);border-bottom-color:var(--blue)}
|
||||
.sbody{flex:1;overflow-y:auto;padding:14px;scrollbar-width:thin}
|
||||
|
||||
/* STAT CARDS */
|
||||
.sgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
||||
.scard{background:var(--bg3);border:1px solid var(--b);border-radius:var(--r);padding:12px 14px;cursor:pointer;transition:all .15s}
|
||||
.scard:hover{border-color:var(--b2)}
|
||||
.scard .num{font-size:24px;font-weight:700;font-family:var(--mono);line-height:1;margin-bottom:4px}
|
||||
.scard .lbl{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.5px}
|
||||
.cb{color:var(--blue)}.cr{color:var(--red)}.cg{color:var(--green)}.ca{color:var(--amber)}.cp{color:var(--purple)}.cro{color:var(--rose)}
|
||||
.slbl{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.8px;margin:16px 0 9px;display:flex;align-items:center;justify-content:space-between}
|
||||
|
||||
/* ITEM CARDS */
|
||||
.ilist{display:flex;flex-direction:column;gap:5px}
|
||||
.icard{background:var(--bg3);border:1px solid var(--b);border-radius:var(--r);padding:10px 12px;cursor:pointer;transition:all .15s}
|
||||
.icard:hover{border-color:var(--b2);background:var(--bg4)}
|
||||
.ihead{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:3px}
|
||||
.iname{font-size:12px;font-weight:600;color:var(--t);line-height:1.3}
|
||||
.isub{font-size:11px;color:var(--t3);margin-bottom:3px;line-height:1.4}
|
||||
.imeta{font-size:10px;color:var(--t3);font-family:var(--mono)}
|
||||
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;white-space:nowrap;flex-shrink:0}
|
||||
.b-g{background:var(--gdim);color:var(--green)}.b-r{background:var(--rdim);color:var(--red)}
|
||||
.b-b{background:var(--bdim);color:var(--blue)}.b-a{background:var(--adim);color:var(--amber)}
|
||||
.b-p{background:var(--pdim);color:var(--purple)}.b-ro{background:var(--rosedim);color:var(--rose)}
|
||||
.b-gray{background:var(--b2);color:var(--t2)}
|
||||
.frow{display:flex;gap:6px;margin-bottom:10px}
|
||||
.frow input,.frow select{flex:1;height:30px;padding:0 10px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:11px;outline:none;transition:all .15s}
|
||||
.frow input:focus,.frow select:focus{border-color:var(--blue)}.frow input::placeholder{color:var(--t3)}
|
||||
.frow select option{background:var(--bg3)}
|
||||
.prog{height:3px;background:var(--bg5);border-radius:2px;overflow:hidden;margin-top:6px}
|
||||
.pfill{height:100%;border-radius:2px;transition:width .4s}
|
||||
|
||||
/* MAP */
|
||||
#map-container{flex:1;position:relative;display:flex;flex-direction:column;overflow:hidden}
|
||||
#map{flex:1}
|
||||
.leaflet-container{background:#0d1120;font-family:var(--font)}
|
||||
.leaflet-control-attribution{background:rgba(10,13,20,.9)!important;color:var(--t3)!important;font-size:9px!important}
|
||||
.leaflet-control-zoom{border:1px solid var(--b2)!important;border-radius:var(--rs)!important;overflow:hidden}
|
||||
.leaflet-control-zoom a{background:var(--bg2)!important;border:none!important;border-bottom:1px solid var(--b)!important;color:var(--t2)!important;width:28px!important;height:28px!important;line-height:28px!important}
|
||||
.leaflet-control-zoom a:last-child{border-bottom:none!important}
|
||||
.leaflet-control-zoom a:hover{background:var(--bg4)!important;color:var(--t)!important}
|
||||
.leaflet-popup-content-wrapper{background:var(--bg2)!important;border:1px solid var(--b2)!important;border-radius:var(--r)!important;box-shadow:var(--shadow)!important;color:var(--t)!important;font-family:var(--font)!important;padding:0!important}
|
||||
.leaflet-popup-content{margin:0!important}
|
||||
.leaflet-popup-tip{background:var(--bg2)!important}
|
||||
.leaflet-popup-close-button{color:var(--t3)!important;top:10px!important;right:12px!important;font-size:16px!important}
|
||||
.mpop{padding:14px 16px;min-width:200px;max-width:256px}
|
||||
.mpop-name{font-size:13px;font-weight:700;margin-bottom:5px;padding-right:20px;letter-spacing:-.2px}
|
||||
.mpop-info{font-size:11px;color:var(--t2);margin-bottom:10px;line-height:1.65}
|
||||
.mpop-btn{width:100%;padding:7px;background:var(--bdim);border:1px solid rgba(79,142,247,.25);border-radius:var(--rs);color:var(--blue);font-family:var(--font);font-size:11px;font-weight:600;cursor:pointer;transition:all .15s}
|
||||
.mpop-btn:hover{background:var(--blue);color:#fff}
|
||||
|
||||
/* MAP LAYER PANEL */
|
||||
#mcp{position:absolute;top:12px;right:12px;z-index:500;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--r);padding:12px 14px;min-width:190px;box-shadow:0 4px 18px rgba(0,0,0,.4)}
|
||||
.mcp-h{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px}
|
||||
.lyr{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:7px;font-size:11px;color:var(--t2)}
|
||||
.lyr:last-of-type{margin-bottom:0}
|
||||
.lyr-l{display:flex;align-items:center;gap:7px}
|
||||
.ldot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.tog{width:30px;height:16px;background:var(--bg5);border-radius:8px;cursor:pointer;position:relative;transition:background .2s;flex-shrink:0;border:1px solid var(--b2)}
|
||||
.tog.on{background:var(--blue);border-color:var(--blue)}
|
||||
.tog::after{content:'';position:absolute;width:11px;height:11px;background:#fff;border-radius:50%;top:2px;left:2px;transition:left .2s}
|
||||
.tog.on::after{left:15px}
|
||||
.mcp-div{height:1px;background:var(--b);margin:10px 0}
|
||||
.rad-lbl{display:flex;justify-content:space-between;font-size:10px;color:var(--t3);margin-bottom:5px}
|
||||
.rad-lbl span:last-child{font-family:var(--mono);color:var(--blue);font-weight:600}
|
||||
input[type=range]{width:100%;height:3px;-webkit-appearance:none;background:var(--bg5);border-radius:2px;outline:none;cursor:pointer}
|
||||
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:var(--blue);border:2px solid var(--bg2);box-shadow:0 0 0 2px var(--bglow)}
|
||||
|
||||
/* MODALS */
|
||||
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;align-items:center;justify-content:center;backdrop-filter:blur(3px)}
|
||||
.overlay.open{display:flex;animation:fi .18s ease}
|
||||
@keyframes fi{from{opacity:0}to{opacity:1}}
|
||||
.modal{background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rl);padding:24px;width:90%;max-width:520px;max-height:90vh;overflow-y:auto;scrollbar-width:thin;animation:su .2s ease;box-shadow:var(--shadow)}
|
||||
.modal.wide{max-width:700px}
|
||||
@keyframes su{from{transform:translateY(16px);opacity:0}to{transform:none;opacity:1}}
|
||||
.mtitle{font-size:16px;font-weight:700;margin-bottom:20px;display:flex;align-items:center;gap:10px;letter-spacing:-.3px}
|
||||
.mico{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.mi-b{background:var(--bdim)}.mi-g{background:var(--gdim)}.mi-a{background:var(--adim)}.mi-r{background:var(--rdim)}
|
||||
.fg{margin-bottom:14px}
|
||||
.fg label{display:block;font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px}
|
||||
.fg input,.fg select,.fg textarea{width:100%;padding:9px 12px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:12px;outline:none;transition:all .18s}
|
||||
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--blue);background:var(--bg4);box-shadow:0 0 0 3px var(--bdim)}
|
||||
.fg input::placeholder{color:var(--t3)}.fg input[readonly]{background:var(--bg);color:var(--t2)}
|
||||
.fg select option{background:var(--bg3)}.fg textarea{resize:vertical;min-height:72px;line-height:1.6}
|
||||
.fr2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.fr3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
|
||||
.fhint{font-size:10px;color:var(--t3);margin-top:6px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
||||
.wbox{background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);border-radius:var(--rs);padding:9px 12px;font-size:11px;color:var(--amber);margin-top:8px;line-height:1.5;display:none}
|
||||
.mfooter{display:flex;gap:8px;justify-content:flex-end;margin-top:20px;padding-top:16px;border-top:1px solid var(--b)}
|
||||
.dtbl{width:100%;border-collapse:collapse;font-size:12px}
|
||||
.dtbl td{padding:7px 0;vertical-align:top;border-bottom:1px solid var(--b);line-height:1.5}
|
||||
.dtbl td:first-child{color:var(--t3);width:40%;padding-right:12px;font-size:11px}
|
||||
.dtbl tr:last-child td{border-bottom:none}
|
||||
.dklist{background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);overflow:hidden;max-height:200px;overflow-y:auto}
|
||||
.dkrow{display:flex;justify-content:space-between;align-items:center;padding:7px 11px;border-bottom:1px solid var(--b);font-size:11px}
|
||||
.dkrow:last-child{border-bottom:none}
|
||||
|
||||
/* CHAT */
|
||||
.chat-msgs{overflow-y:auto;display:flex;flex-direction:column;gap:10px;min-height:300px;max-height:420px;padding:2px 0}
|
||||
.cmsg{max-width:82%}
|
||||
.cmsg.mine{align-self:flex-end}.cmsg.other{align-self:flex-start}
|
||||
.cmsg-meta{font-size:9px;color:var(--t3);margin-bottom:3px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
|
||||
.cmsg.mine .cmsg-meta{text-align:right}
|
||||
.cmsg-bubble{padding:9px 13px;border-radius:10px;font-size:12px;line-height:1.6}
|
||||
.cmsg.mine .cmsg-bubble{background:var(--blue);color:#fff;border-bottom-right-radius:3px}
|
||||
.cmsg.other .cmsg-bubble{background:var(--bg4);border:1px solid var(--b2);border-bottom-left-radius:3px}
|
||||
.cmsg-time{font-size:9px;opacity:.55;margin-top:3px;font-family:var(--mono)}
|
||||
.cmsg.mine .cmsg-time{text-align:right}
|
||||
.cinp-row{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--b)}
|
||||
.cinp{flex:1;padding:9px 12px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:12px;outline:none;resize:none;height:38px;transition:all .15s;line-height:1.4}
|
||||
.cinp:focus{border-color:var(--blue);background:var(--bg4)}
|
||||
|
||||
/* DATA TABLE */
|
||||
.dtable{width:100%;border-collapse:collapse;font-size:12px}
|
||||
.dtable thead tr{background:var(--bg3)}
|
||||
.dtable th{padding:9px 12px;text-align:left;font-weight:700;font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:var(--t2);border-bottom:1px solid var(--b2);white-space:nowrap}
|
||||
.dtable td{padding:9px 12px;border-bottom:1px solid var(--b);vertical-align:middle;line-height:1.4}
|
||||
.dtable tr:hover td{background:rgba(255,255,255,.015)}
|
||||
.dtable tr:last-child td{border-bottom:none}
|
||||
|
||||
/* PAGE */
|
||||
.pwrap{flex:1;overflow-y:auto;background:var(--bg);padding:20px}
|
||||
.pinner{max-width:960px;margin:0 auto}
|
||||
.pheader{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;gap:12px;flex-wrap:wrap}
|
||||
.ptitle{font-size:20px;font-weight:700;letter-spacing:-.5px}
|
||||
.ptitle small{font-size:13px;font-weight:400;color:var(--t3);margin-left:8px}
|
||||
.scard2{background:var(--bg2);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:14px}
|
||||
.scard2 h3{font-size:13px;font-weight:600;margin-bottom:14px;letter-spacing:-.2px;display:flex;align-items:center;gap:8px;color:var(--t2)}
|
||||
|
||||
/* URGENCY */
|
||||
.u-darurat{background:var(--rosedim);color:var(--rose)}
|
||||
.u-tinggi{background:var(--rdim);color:var(--red)}
|
||||
.u-sedang{background:var(--adim);color:var(--amber)}
|
||||
.u-rendah{background:var(--gdim);color:var(--green)}
|
||||
|
||||
/* TOAST */
|
||||
#toast{position:fixed;bottom:20px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;max-width:340px}
|
||||
.ti{padding:11px 15px;border-radius:var(--rs);font-size:12px;font-weight:500;animation:tin .25s ease;pointer-events:auto;line-height:1.5;box-shadow:0 4px 16px rgba(0,0,0,.4)}
|
||||
@keyframes tin{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
|
||||
.t-ok{background:#15803d;color:#fff}.t-err{background:#b91c1c;color:#fff}
|
||||
.t-info{background:var(--bg4);color:var(--t);border:1px solid var(--b2)}.t-warn{background:#92400e;color:#fef3c7}
|
||||
|
||||
/* LOADING */
|
||||
#loading{position:fixed;inset:0;background:rgba(10,13,20,.8);z-index:99998;display:none;align-items:center;justify-content:center;flex-direction:column;gap:14px}
|
||||
#loading.show{display:flex}
|
||||
.spin{width:32px;height:32px;border:2px solid var(--b2);border-top-color:var(--blue);border-radius:50%;animation:sp .65s linear infinite}
|
||||
@keyframes sp{to{transform:rotate(360deg)}}
|
||||
.empty{text-align:center;padding:32px 16px;color:var(--t3)}
|
||||
.empty svg{opacity:.2;margin:0 auto 10px;display:block}
|
||||
.empty p{font-size:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="loading"><div class="spin"></div><span style="font-size:12px;color:var(--t2)">Memuat sistem...</span></div>
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- ===== LOGIN ===== -->
|
||||
<div id="login-screen">
|
||||
<div class="lcard">
|
||||
<div class="lbrand">
|
||||
<div class="lbrand-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg>
|
||||
</div>
|
||||
<div><h1>WebGIS Pengentasan Kemiskinan</h1><p>Kota Pontianak · v3.0</p></div>
|
||||
</div>
|
||||
<div class="lheading">Masuk ke Sistem</div>
|
||||
<p class="lsub">Masukkan username dan password yang telah diberikan oleh administrator sistem.</p>
|
||||
<div class="lerr" id="lerr"></div>
|
||||
<div class="lf">
|
||||
<label>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
Username
|
||||
</label>
|
||||
<input type="text" id="lu" placeholder="Masukkan username" autocomplete="username">
|
||||
</div>
|
||||
<div class="lf">
|
||||
<label>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Password
|
||||
</label>
|
||||
<input type="password" id="lp" placeholder="Masukkan password" autocomplete="current-password">
|
||||
</div>
|
||||
<button class="lbtn" id="lbtn" onclick="doLogin()">Masuk ke Sistem</button>
|
||||
<div class="laccs">
|
||||
<div class="laccs-title">Akun Tersedia — semua password: <span style="font-family:var(--mono);color:var(--blue)">password</span></div>
|
||||
<table>
|
||||
<tr><td>Super Admin</td><td>superadmin</td></tr>
|
||||
<tr><td>Pimpinan Daerah</td><td>pimpinan1</td></tr>
|
||||
<tr><td>Pengurus Masjid</td><td>pengurus1</td></tr>
|
||||
<tr><td>Pengurus Gereja</td><td>pengurus2</td></tr>
|
||||
<tr><td>Pengurus Al-Falah</td><td>pengurus3</td></tr>
|
||||
<tr><td>Relawan Lapangan</td><td>relawan1</td></tr>
|
||||
<tr><td>Masyarakat Umum</td><td>masyarakat1</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TOPBAR ===== -->
|
||||
<div id="topbar">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleSB()" style="padding:0 9px">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
</button>
|
||||
<div class="brand">
|
||||
<div class="brand-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg>
|
||||
</div>
|
||||
<div><h1>WebGIS Kemiskinan — Kota Pontianak</h1><p>Sistem Pemetaan & Distribusi Bantuan Sosial Berbasis Komunitas</p></div>
|
||||
</div>
|
||||
<button class="nav-btn" id="nb-peta" onclick="goPage('peta')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/></svg>
|
||||
Peta
|
||||
</button>
|
||||
<button class="nav-btn" id="nb-penduduk" onclick="goPage('penduduk')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Penduduk
|
||||
</button>
|
||||
<button class="nav-btn" id="nb-bantuan" onclick="goPage('bantuan')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>
|
||||
Bantuan
|
||||
</button>
|
||||
<button class="nav-btn" id="nb-laporan" onclick="goPage('laporan')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
Laporan
|
||||
<span class="ndot" id="dlap" style="display:none"></span>
|
||||
</button>
|
||||
<button class="nav-btn" id="nb-chat" onclick="goPage('chat')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
Koordinasi
|
||||
<span class="ndot" id="dchat" style="display:none"></span>
|
||||
</button>
|
||||
<div class="tsep"></div>
|
||||
<div class="upill">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--t3)"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span class="uname" id="uname">—</span>
|
||||
<span class="rbadge" id="urole">—</span>
|
||||
<button class="btn btn-ghost btn-xs" onclick="doLogout()" title="Keluar" style="padding:0 6px;margin-left:2px">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MAIN APP ===== -->
|
||||
<div id="app">
|
||||
<div id="sidebar">
|
||||
<div class="stabs" id="stabs"></div>
|
||||
<div class="sbody" id="sbody"></div>
|
||||
</div>
|
||||
<div id="map-container">
|
||||
<div id="map"></div>
|
||||
<div id="mcp">
|
||||
<div class="mcp-h">Layer Peta</div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#f87171"></div>Penduduk Miskin</div><div class="tog on" id="ly-miskin" onclick="togLyr(this,'miskin')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fbbf24"></div>Penduduk Rentan</div><div class="tog on" id="ly-rentan" onclick="togLyr(this,'rentan')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#34d399"></div>Penduduk Mampu</div><div class="tog" id="ly-mampu" onclick="togLyr(this,'mampu')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#4f8ef7"></div>Rumah Ibadah</div><div class="tog on" id="ly-ri" onclick="togLyr(this,'ri')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fb7185"></div>Laporan Warga</div><div class="tog on" id="ly-lap" onclick="togLyr(this,'laporan')"></div></div>
|
||||
<div class="mcp-div"></div>
|
||||
<div class="rad-lbl"><span>Radius Distribusi</span><span id="rv">300 m</span></div>
|
||||
<input type="range" min="100" max="1000" step="50" value="300" id="rsl" oninput="setRad(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MODAL KK ===== -->
|
||||
<div class="overlay" id="modal-kk">
|
||||
<div class="modal">
|
||||
<div class="mtitle"><div class="mico mi-b"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--blue)" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div><span id="mkk-t">Tambah Kartu Keluarga</span></div>
|
||||
<div class="fr2"><div class="fg"><label>No. KK (16 digit)</label><input type="text" id="kk-nokk" maxlength="16" placeholder="617101XXXXXXXXXX"></div><div class="fg"><label>Nama Kepala Keluarga</label><input type="text" id="kk-nama" placeholder="Nama sesuai KK"></div></div>
|
||||
<div class="fg"><label>Alamat Lengkap</label><input type="text" id="kk-alamat" placeholder="Jl. ..."></div>
|
||||
<div class="fr3"><div class="fg"><label>RT</label><input type="text" id="kk-rt" maxlength="4" placeholder="03"></div><div class="fg"><label>RW</label><input type="text" id="kk-rw" maxlength="4" placeholder="05"></div><div class="fg"><label>Status Ekonomi</label><select id="kk-se"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div></div>
|
||||
<div class="fr2"><div class="fg"><label>Kelurahan</label><input type="text" id="kk-kel" placeholder="Saigon"></div><div class="fg"><label>Kecamatan</label><input type="text" id="kk-kec" placeholder="Pontianak Timur"></div></div>
|
||||
<div class="fr2"><div class="fg"><label>Latitude</label><input type="number" id="kk-lat" step="0.000001" placeholder="-0.021100"></div><div class="fg"><label>Longitude</label><input type="number" id="kk-lng" step="0.000001" placeholder="109.344500"></div></div>
|
||||
<div class="fhint">
|
||||
<button class="btn btn-sm" onclick="pickFor('kk')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg> Pilih di Peta</button>
|
||||
<button class="btn btn-sm" onclick="gpsTo('kk')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg> GPS Saya</button>
|
||||
</div>
|
||||
<div class="fg" style="margin-top:12px"><label>Rumah Ibadah Terdekat</label><select id="kk-ri"><option value="">-- Pilih --</option></select></div>
|
||||
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('kk')">Batal</button><button class="btn btn-primary" onclick="saveKK()">Simpan KK</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MODAL PENDUDUK ===== -->
|
||||
<div class="overlay" id="modal-p">
|
||||
<div class="modal wide">
|
||||
<div class="mtitle"><div class="mico mi-g"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><span id="mp-t">Tambah Data Penduduk</span></div>
|
||||
<div class="fr2"><div class="fg"><label>NIK (16 digit)</label><input type="text" id="p-nik" maxlength="16" placeholder="6171XXXXXXXXXXXX"></div><div class="fg"><label>Nama Lengkap</label><input type="text" id="p-nama" placeholder="Nama sesuai KTP"></div></div>
|
||||
<div class="fr3"><div class="fg"><label>Jenis Kelamin</label><select id="p-jk"><option value="L">Laki-laki</option><option value="P">Perempuan</option></select></div><div class="fg"><label>Tanggal Lahir</label><input type="date" id="p-tgl" oninput="cekPend()"></div><div class="fg"><label>Status dalam KK</label><select id="p-sk"><option value="kepala">Kepala KK</option><option value="istri">Istri</option><option value="anak">Anak</option><option value="orang_tua">Orang Tua</option><option value="lainnya">Lainnya</option></select></div></div>
|
||||
<div class="fr3"><div class="fg"><label>Status Perkawinan</label><select id="p-sp"><option value="belum_kawin">Belum Kawin</option><option value="kawin">Kawin</option><option value="cerai_hidup">Cerai Hidup</option><option value="cerai_mati">Cerai Mati</option></select></div><div class="fg"><label>Status Hidup</label><select id="p-sh"><option value="hidup">Hidup</option><option value="meninggal">Meninggal</option></select></div><div class="fg"><label>Status Ekonomi</label><select id="p-se"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div></div>
|
||||
<div class="fr3"><div class="fg"><label>Pendidikan Terakhir</label><select id="p-pend" onchange="cekPend()"><option value="tidak_sekolah">Tidak Sekolah</option><option value="sd">SD</option><option value="smp">SMP</option><option value="sma">SMA</option><option value="smk">SMK</option><option value="d3">D3</option><option value="s1">S1</option><option value="s2">S2</option><option value="s3">S3</option></select></div><div class="fg"><label>Status Pendidikan</label><select id="p-spend" onchange="cekPend()"><option value="tidak_sekolah">Sudah / Tidak Sekolah</option><option value="masih_sekolah">Masih Sekolah</option></select></div><div class="fg"><label>Kondisi Kesehatan</label><select id="p-kes"><option value="sehat">Sehat</option><option value="sakit_ringan">Sakit Ringan</option><option value="sakit_berat">Sakit Berat</option><option value="disabilitas">Disabilitas</option></select></div></div>
|
||||
<div class="wbox" id="wp"></div>
|
||||
<div class="fr2"><div class="fg"><label>Pekerjaan</label><input type="text" id="p-kerja" placeholder="Buruh Harian, Nelayan..."></div><div class="fg"><label>Penghasilan / Bulan (Rp)</label><input type="number" id="p-gaji" placeholder="0" min="0"></div></div>
|
||||
<div class="fg"><label>Kartu Keluarga (KK)</label><select id="p-kk"><option value="">-- Pilih KK --</option></select></div>
|
||||
<div class="fg"><label>Catatan</label><textarea id="p-cat" placeholder="Kondisi khusus atau keterangan tambahan..."></textarea></div>
|
||||
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('p')">Batal</button><button class="btn btn-success" onclick="saveP()">Simpan Penduduk</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MODAL LAPORAN ===== -->
|
||||
<div class="overlay" id="modal-lap">
|
||||
<div class="modal">
|
||||
<div class="mtitle"><div class="mico mi-r"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--red)" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>Buat Laporan Kondisi Warga</div>
|
||||
<div class="fr2"><div class="fg"><label>Nama Pelapor <span style="opacity:.5;font-weight:400">(opsional)</span></label><input type="text" id="lap-nama" placeholder="Nama atau Anonim"></div><div class="fg"><label>Telepon <span style="opacity:.5;font-weight:400">(opsional)</span></label><input type="text" id="lap-telp" placeholder="08xx-xxxx-xxxx"></div></div>
|
||||
<div class="fg"><label>Deskripsi Kondisi yang Dilaporkan</label><textarea id="lap-desc" placeholder="Jelaskan kondisi warga secara detail..."></textarea></div>
|
||||
<div class="fg"><label>Alamat Lokasi</label><input type="text" id="lap-alamat" placeholder="Jl. ... RT/RW Kelurahan ..."></div>
|
||||
<div class="fr2"><div class="fg"><label>Latitude</label><input type="number" id="lap-lat" step="0.000001"></div><div class="fg"><label>Longitude</label><input type="number" id="lap-lng" step="0.000001"></div></div>
|
||||
<div class="fhint">
|
||||
<button class="btn btn-sm" onclick="pickFor('lap')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg> Pilih di Peta</button>
|
||||
<button class="btn btn-sm" onclick="gpsTo('lap')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg> GPS Saya</button>
|
||||
</div>
|
||||
<div class="fr2" style="margin-top:12px"><div class="fg"><label>Tingkat Urgensi</label><select id="lap-urg"><option value="rendah">Rendah</option><option value="sedang" selected>Sedang</option><option value="tinggi">Tinggi</option><option value="darurat">DARURAT</option></select></div><div class="fg"><label>Foto <span style="opacity:.5;font-weight:400">maks 5MB</span></label><input type="file" id="lap-foto" accept="image/*" style="padding:6px;font-size:11px"></div></div>
|
||||
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('lap')">Batal</button><button class="btn btn-danger" onclick="saveLap()">Kirim Laporan</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MODAL DETAIL ===== -->
|
||||
<div class="overlay" id="modal-det">
|
||||
<div class="modal wide">
|
||||
<div class="mtitle" id="det-h"></div>
|
||||
<div id="det-b"></div>
|
||||
<div class="mfooter" id="det-f"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const EP={auth:'api/auth.php',p:'api/penduduk.php',kk:'api/keluarga.php',ri:'api/rumah_ibadah.php',b:'api/bantuan.php',lap:'api/laporan.php',chat:'api/chat.php',stats:'api/stats.php'};
|
||||
const PT=[-0.0263,109.3425];
|
||||
const EC={miskin:'#f87171',rentan:'#fbbf24',mampu:'#34d399'};
|
||||
const UC={darurat:'#fb7185',tinggi:'#f87171',sedang:'#fbbf24',rendah:'#34d399'};
|
||||
let map,cu=null,riD=[],pD=[],lapD=[],bD=[],kkD=[];
|
||||
let lyrs={miskin:true,rentan:true,mampu:false,ri:true,laporan:true};
|
||||
let rad=300,pick=null,eKK=null,eP=null;
|
||||
let mk={ri:[],p:[],lap:[],c:[]};
|
||||
let cpoll=null,lastC='1970-01-01 00:00:00',curPg='peta';
|
||||
|
||||
const ld=v=>document.getElementById('loading').classList.toggle('show',v);
|
||||
function toast(m,t='info'){const e=document.createElement('div');e.className='ti t-'+t;e.textContent=m;document.getElementById('toast').appendChild(e);setTimeout(()=>e.remove(),3800);}
|
||||
async function rq(ep,method='GET',body=null,qs=''){
|
||||
const r=await fetch(ep+(qs?'?'+qs:''),{method,headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
|
||||
const d=await r.json();if(!d.success)throw new Error(d.message||'Error');return d.data;
|
||||
}
|
||||
|
||||
// SVG helpers
|
||||
const riSVG=j=>{const m=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 20h20M4 20V10c0-1.1.4-2.1 1.2-2.8L12 3l6.8 4.2c.8.7 1.2 1.7 1.2 2.8v10"/><path d="M9 20v-5a3 3 0 0 1 6 0v5"/><path d="M12 3V1m-4 3L6 2m10 2 2-2"/></svg>`;const g=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 20h18M5 20V9.5L12 4l7 5.5V20"/><path d="M9 20v-5h6v5"/><line x1="12" y1="4" x2="12" y2="1"/><line x1="10" y1="2" x2="14" y2="2"/></svg>`;const v=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 20h20M4 20v-7l8-9 8 9v7"/><path d="M9 20v-4h6v4"/><path d="M1 13h3m16 0h3"/></svg>`;if(!j)return m;const k=j.toLowerCase();return k==='masjid'?m:k==='gereja'?g:v;};
|
||||
const icoUser=`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
|
||||
const icoHome=`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`;
|
||||
const icoWarn=`<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`;
|
||||
const icoGift=`<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>`;
|
||||
|
||||
// LOGIN
|
||||
async function doLogin(){
|
||||
const u=document.getElementById('lu').value.trim(),p=document.getElementById('lp').value;
|
||||
const err=document.getElementById('lerr'),btn=document.getElementById('lbtn');
|
||||
err.style.display='none';
|
||||
if(!u||!p){err.textContent='Username dan password wajib diisi.';err.style.display='block';return;}
|
||||
btn.disabled=true;btn.textContent='Memverifikasi...';
|
||||
try{
|
||||
cu=await rq(EP.auth,'POST',{username:u,password:p},'action=login');
|
||||
applyRole();document.getElementById('login-screen').classList.add('hidden');
|
||||
toast('Selamat datang, '+cu.nama_lengkap,'ok');
|
||||
initApp();
|
||||
}catch(e){err.textContent=e.message;err.style.display='block';btn.disabled=false;btn.textContent='Masuk ke Sistem';}
|
||||
}
|
||||
async function doLogout(){
|
||||
try{await rq(EP.auth,'POST',null,'action=logout');}catch(e){}
|
||||
if(cpoll)clearInterval(cpoll);cu=null;if(map){map.remove();map=null;}
|
||||
document.getElementById('login-screen').classList.remove('hidden');
|
||||
document.getElementById('lp').value='';document.getElementById('lerr').style.display='none';
|
||||
document.getElementById('lbtn').disabled=false;document.getElementById('lbtn').textContent='Masuk ke Sistem';
|
||||
}
|
||||
function applyRole(){
|
||||
if(!cu)return;
|
||||
document.getElementById('uname').textContent=cu.nama_lengkap;
|
||||
const rb=document.getElementById('urole');rb.textContent=cu.role_nama;rb.className='rbadge r-'+cu.role;
|
||||
const H=id=>{const e=document.getElementById(id);if(e)e.style.display='none';};
|
||||
if(cu.role==='masyarakat'){H('nb-bantuan');H('nb-chat');}
|
||||
if(cu.role==='pimpinan'){H('nb-bantuan');H('nb-chat');}
|
||||
}
|
||||
|
||||
// APP INIT
|
||||
function initApp(){
|
||||
initMap();loadAll();
|
||||
if(['superadmin','pengurus','relawan'].includes(cu.role)) cpoll=setInterval(pollChat,8000);
|
||||
buildSB();goPage('peta');
|
||||
}
|
||||
|
||||
// MAP
|
||||
function initMap(){
|
||||
map=L.map('map',{zoomControl:true}).setView(PT,13);
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© OSM © CARTO',maxZoom:19}).addTo(map);
|
||||
map.on('click',onMapClick);
|
||||
}
|
||||
function onMapClick(e){
|
||||
if(!pick)return;
|
||||
const{lat,lng}=e.latlng;
|
||||
if(pick==='kk'){document.getElementById('kk-lat').value=lat.toFixed(6);document.getElementById('kk-lng').value=lng.toFixed(6);map.closePopup();openM('kk');}
|
||||
if(pick==='lap'){document.getElementById('lap-lat').value=lat.toFixed(6);document.getElementById('lap-lng').value=lng.toFixed(6);map.closePopup();openM('lap');}
|
||||
pick=null;map.getContainer().style.cursor='';
|
||||
}
|
||||
function pickFor(t){pick=t;map.getContainer().style.cursor='crosshair';closeM(t==='kk'?'kk':'lap');L.popup({closeButton:false}).setLatLng(map.getCenter()).setContent('<div style="background:var(--bg3);border:1px solid var(--b2);border-radius:8px;padding:10px 14px;color:var(--t);font-size:12px">Klik lokasi di peta untuk memilih koordinat</div>').openOn(map);}
|
||||
function gpsTo(t){if(!navigator.geolocation){toast('GPS tidak didukung','err');return;}navigator.geolocation.getCurrentPosition(pos=>{const pref=t==='kk'?'kk-':'lap-';document.getElementById(pref+'lat').value=pos.coords.latitude.toFixed(6);document.getElementById(pref+'lng').value=pos.coords.longitude.toFixed(6);},()=>toast('Gagal mengambil GPS','err'));}
|
||||
function setRad(v){rad=parseInt(v);document.getElementById('rv').textContent=v+' m';mk.c.forEach(c=>c.setRadius(rad));}
|
||||
function togLyr(el,t){el.classList.toggle('on');lyrs[t]=el.classList.contains('on');rerender();}
|
||||
|
||||
// LOAD
|
||||
async function loadAll(){
|
||||
ld(true);
|
||||
try{
|
||||
const[ri,p,lap,b,kk]=await Promise.all([rq(EP.ri),rq(EP.p),rq(EP.lap),rq(EP.b),rq(EP.kk)]);
|
||||
riD=ri||[];pD=p||[];lapD=lap||[];bD=b||[];kkD=kk||[];
|
||||
rerender();rSB();checkAlerts();toast('Data berhasil dimuat','ok');
|
||||
}catch(e){toast('Gagal memuat data: '+e.message,'err');}
|
||||
finally{ld(false);}
|
||||
}
|
||||
|
||||
// MARKERS
|
||||
function clrMk(){[...mk.ri,...mk.p,...mk.lap].forEach(m=>map&&map.removeLayer(m));mk.c.forEach(c=>map&&map.removeLayer(c));mk.ri=[];mk.p=[];mk.lap=[];mk.c=[];}
|
||||
function rerender(){
|
||||
clrMk();
|
||||
if(lyrs.ri) riD.forEach(addRI);
|
||||
pD.forEach(p=>{if(lyrs[p.status_ekonomi]) addP(p);});
|
||||
if(lyrs.laporan) lapD.forEach(l=>{if(l.lat&&l.lng) addLap(l);});
|
||||
}
|
||||
function addRI(ri){
|
||||
const m=L.marker([ri.lat,ri.lng],{icon:L.divIcon({html:`<div style="width:34px;height:34px;background:#111520;border:2px solid #4f8ef7;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#4f8ef7;box-shadow:0 0 0 4px rgba(79,142,247,.18),0 3px 10px rgba(0,0,0,.6)">${riSVG(ri.jenis)}</div>`,iconSize:[34,34],iconAnchor:[17,17],className:''})}).addTo(map);
|
||||
m.bindPopup(`<div class="mpop"><div class="mpop-name">${ri.nama}</div><div class="mpop-info">${ri.jenis} · ${ri.kelurahan||'—'}<br>${ri.kontak||'—'}<br><strong>${ri.kk_miskin||0}</strong> KK miskin dalam jangkauan</div><button class="mpop-btn" onclick="detRI(${ri.id});map.closePopup()">Lihat Detail</button></div>`,{maxWidth:240});
|
||||
const c=L.circle([ri.lat,ri.lng],{radius:rad,color:'#4f8ef7',fillColor:'#4f8ef7',fillOpacity:.05,dashArray:'5,5',weight:1.5}).addTo(map);
|
||||
mk.ri.push(m);mk.c.push(c);
|
||||
}
|
||||
function addP(p){
|
||||
const col=EC[p.status_ekonomi]||'#888';const sk=p.kondisi_kesehatan&&p.kondisi_kesehatan!=='sehat';
|
||||
const m=L.marker([p.lat||0,p.lng||0],{icon:L.divIcon({html:`<div style="width:12px;height:12px;background:${col};border-radius:50%;border:2px solid rgba(255,255,255,.8);box-shadow:0 0 0 3px ${sk?'rgba(251,113,133,.5)':'rgba(255,255,255,.1)'},0 2px 6px rgba(0,0,0,.5)"></div>`,iconSize:[12,12],iconAnchor:[6,6],className:''})}).addTo(map);
|
||||
m.bindPopup(`<div class="mpop"><div class="mpop-name">${p.nama}</div><div class="mpop-info">NIK: ${p.nik||'—'}<br>${p.umur||'?'} thn · ${p.profil_umur||'—'}<br>Ekonomi: <strong style="color:${col}">${(p.status_ekonomi||'').toUpperCase()}</strong><br>Kesehatan: ${p.kondisi_kesehatan||'sehat'}<br>Bantuan: ${p.total_bantuan||0}x</div><button class="mpop-btn" onclick="detP(${p.id});map.closePopup()">Lihat Detail Lengkap</button></div>`,{maxWidth:240});
|
||||
mk.p.push(m);
|
||||
}
|
||||
function addLap(l){
|
||||
const col=UC[l.urgensi]||'#888';
|
||||
const m=L.marker([l.lat,l.lng],{icon:L.divIcon({html:`<div style="width:17px;height:17px;background:${col};border-radius:50%;border:2.5px solid #fff;box-shadow:0 0 0 3px ${col}44,0 3px 8px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center">${l.urgensi==='darurat'?'<span style="font-size:9px;color:#fff;font-weight:900;line-height:1">!</span>':''}</div>`,iconSize:[17,17],iconAnchor:[8,8],className:''})}).addTo(map);
|
||||
m.bindPopup(`<div class="mpop"><div class="mpop-name">Laporan Warga</div><div class="mpop-info"><span class="badge u-${l.urgensi}" style="display:inline-flex;margin-bottom:5px">${(l.urgensi||'').toUpperCase()}</span><br>${(l.deskripsi||'').substring(0,80)}${(l.deskripsi||'').length>80?'…':''}</div><button class="mpop-btn" onclick="detLap(${l.id});map.closePopup()">Lihat Detail</button></div>`,{maxWidth:240});
|
||||
mk.lap.push(m);
|
||||
}
|
||||
|
||||
// SIDEBAR
|
||||
function buildSB(){
|
||||
document.getElementById('stabs').innerHTML=['Ringkasan','Ibadah','Keluarga','Prioritas'].map((t,i)=>`<div class="stab${i===0?' active':''}" onclick="showTab(${i})">${t}</div>`).join('');
|
||||
showTab(0);
|
||||
}
|
||||
function showTab(i){
|
||||
document.querySelectorAll('.stab').forEach((e,j)=>e.classList.toggle('active',i===j));
|
||||
if(i===0)rDash();else if(i===1)rRITab();else if(i===2)rKKTab();else rPrioTab();
|
||||
}
|
||||
function rSB(){const i=[...document.querySelectorAll('.stab')].findIndex(e=>e.classList.contains('active'));showTab(i>=0?i:0);}
|
||||
|
||||
function rDash(){
|
||||
const mis=pD.filter(p=>p.status_ekonomi==='miskin').length,ren=pD.filter(p=>p.status_ekonomi==='rentan').length;
|
||||
const bel=pD.filter(p=>['miskin','rentan'].includes(p.status_ekonomi)&&!parseInt(p.total_bantuan)).length;
|
||||
const lAkt=lapD.filter(l=>l.status!=='selesai').length,lDar=lapD.filter(l=>l.urgensi==='darurat'&&l.status!=='selesai').length;
|
||||
document.getElementById('sbody').innerHTML=`
|
||||
<div class="sgrid">
|
||||
<div class="scard" onclick="goPage('penduduk')"><div class="num cb">${pD.length}</div><div class="lbl">Total Penduduk</div></div>
|
||||
<div class="scard"><div class="num">${kkD.length}</div><div class="lbl">Total KK</div></div>
|
||||
<div class="scard"><div class="num cr">${mis}</div><div class="lbl">Miskin</div></div>
|
||||
<div class="scard"><div class="num ca">${ren}</div><div class="lbl">Rentan</div></div>
|
||||
<div class="scard"><div class="num cro">${bel}</div><div class="lbl">Belum Dibantu</div></div>
|
||||
<div class="scard" onclick="goPage('laporan')"><div class="num cp">${lAkt}${lDar?`<span style="font-size:11px;color:var(--rose)"> (${lDar}!)</span>`:''}</div><div class="lbl">Laporan Aktif</div></div>
|
||||
</div>
|
||||
<div class="slbl">Jangkauan per Rumah Ibadah</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
${riD.map(ri=>{const k=kkD.filter(x=>x.rumah_ibadah_id==ri.id);const m=k.filter(x=>x.status_ekonomi==='miskin').length;const p=k.length?Math.round(m/k.length*100):0;return`<div style="background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);padding:9px 11px"><div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:4px"><span style="display:flex;align-items:center;gap:5px;color:var(--t2);font-weight:500">${riSVG(ri.jenis)} ${ri.nama.length>22?ri.nama.substring(0,22)+'…':ri.nama}</span><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${k.length} KK</span></div><div class="prog"><div class="pfill" style="width:${p}%;background:var(--red)"></div></div></div>`;}).join('')}
|
||||
</div>
|
||||
<div class="slbl" style="margin-top:18px">Legenda</div>
|
||||
<div style="display:flex;flex-direction:column;gap:7px;font-size:11px;color:var(--t2)">
|
||||
${[['#f87171','Penduduk Miskin'],['#fbbf24','Penduduk Rentan'],['#34d399','Penduduk Mampu'],['#4f8ef7','Rumah Ibadah'],['#fb7185','Laporan Darurat']].map(([c,l])=>`<div style="display:flex;align-items:center;gap:8px"><div style="width:9px;height:9px;background:${c};border-radius:50%;flex-shrink:0"></div>${l}</div>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function rRITab(){
|
||||
const cA=cu?.role==='superadmin';
|
||||
document.getElementById('sbody').innerHTML=`
|
||||
${cA?`<button class="btn btn-primary btn-sm" style="width:100%;justify-content:center;margin-bottom:10px" onclick="toast('Fitur ini untuk Super Admin','info')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah Rumah Ibadah</button>`:''}
|
||||
<div class="frow"><input type="text" id="q-ri" placeholder="Cari nama..." oninput="fRI()"></div>
|
||||
<div class="ilist" id="l-ri">${riCards(riD)}</div>`;
|
||||
}
|
||||
function riCards(d){if(!d.length)return`<div class="empty"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><p>Tidak ada data</p></div>`;return d.map(r=>{const m=kkD.filter(k=>k.rumah_ibadah_id==r.id&&k.status_ekonomi==='miskin').length;return`<div class="icard" onclick="detRI(${r.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px;color:var(--blue)">${riSVG(r.jenis)} ${r.nama}</div><span class="badge b-b">${r.jenis}</span></div><div class="isub">${r.kelurahan||'—'}</div><div class="imeta">${r.jumlah_kk||0} KK  ·  ${m} miskin</div></div>`;}).join('');}
|
||||
function fRI(){const q=(document.getElementById('q-ri')?.value||'').toLowerCase();const el=document.getElementById('l-ri');if(el)el.innerHTML=riCards(riD.filter(r=>r.nama.toLowerCase().includes(q)));}
|
||||
|
||||
function rKKTab(){
|
||||
const cW=['superadmin','pengurus','relawan'].includes(cu?.role);
|
||||
document.getElementById('sbody').innerHTML=`
|
||||
${cW?`<div style="display:flex;gap:6px;margin-bottom:10px">
|
||||
<button class="btn btn-primary btn-sm" style="flex:1;justify-content:center" onclick="openAddKK()"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> KK</button>
|
||||
<button class="btn btn-success btn-sm" style="flex:1;justify-content:center" onclick="openAddP()"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Penduduk</button>
|
||||
</div>`:''}
|
||||
<div class="frow"><input type="text" id="q-kk" placeholder="Cari nama / No. KK..." oninput="fKK()"></div>
|
||||
<div class="frow"><select id="fse-kk" onchange="fKK()"><option value="">Semua Status</option><option value="miskin">Miskin</option><option value="rentan">Rentan</option><option value="mampu">Mampu</option></select></div>
|
||||
<div class="ilist" id="l-kk">${kkCards(kkD)}</div>`;
|
||||
}
|
||||
function kkCards(d){if(!d.length)return`<div class="empty"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg><p>Tidak ada data</p></div>`;return d.slice(0,60).map(k=>`<div class="icard" onclick="detKK(${k.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px">${icoHome} ${k.nama_kk}</div><span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}">${k.status_ekonomi}</span></div><div class="isub">${(k.alamat||'—').substring(0,38)}</div><div class="imeta">${k.jumlah_jiwa||0} jiwa  ·  Bantuan: ${k.total_bantuan_diterima||0}x</div></div>`).join('');}
|
||||
function fKK(){const q=(document.getElementById('q-kk')?.value||'').toLowerCase();const fse=document.getElementById('fse-kk')?.value||'';const f=kkD.filter(k=>(!q||(k.nama_kk.toLowerCase().includes(q)||k.no_kk.includes(q)))&&(!fse||k.status_ekonomi===fse));const el=document.getElementById('l-kk');if(el)el.innerHTML=kkCards(f);}
|
||||
|
||||
async function rPrioTab(){
|
||||
document.getElementById('sbody').innerHTML=`<div class="empty" style="margin-top:50px"><div class="spin" style="margin:0 auto 12px"></div><p>Menganalisis prioritas...</p></div>`;
|
||||
try{
|
||||
const d=await rq(EP.b,'GET',null,'action=prioritas');
|
||||
document.getElementById('sbody').innerHTML=`
|
||||
<div style="font-size:10px;color:var(--t3);margin-bottom:10px;padding:10px;background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);line-height:1.6">Warga miskin dengan penyakit berat atau lansia yang hidup sendiri mendapat prioritas tertinggi.</div>
|
||||
<div class="ilist">${d.slice(0,30).map(p=>{const c=p.prioritas==='SANGAT TINGGI'?'b-ro':p.prioritas==='TINGGI'?'b-r':p.prioritas==='SEDANG'?'b-a':'b-g';return`<div class="icard" onclick="detP(${p.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px">${icoUser} ${p.nama}</div><span class="badge ${c}">${p.prioritas}</span></div><div class="isub">${p.umur||'?'} thn · ${p.kondisi_kesehatan||'—'}</div><div class="imeta">${p.kelurahan||'—'} · Bantuan: ${p.total_bantuan||0}x</div></div>`;}).join('')}</div>`;
|
||||
}catch(e){toast(e.message,'err');}
|
||||
}
|
||||
|
||||
// PAGES
|
||||
function goPage(pg){
|
||||
curPg=pg;
|
||||
['peta','penduduk','bantuan','laporan','chat'].forEach(x=>document.getElementById('nb-'+x)?.classList.remove('active'));
|
||||
document.getElementById('nb-'+pg)?.classList.add('active');
|
||||
if(pg==='peta') rebuildPeta();
|
||||
else if(pg==='penduduk') pgPenduduk();
|
||||
else if(pg==='bantuan') pgBantuan();
|
||||
else if(pg==='laporan') pgLaporan();
|
||||
else if(pg==='chat') pgChat();
|
||||
}
|
||||
|
||||
function rebuildPeta(){
|
||||
document.getElementById('app').innerHTML=`
|
||||
<div id="sidebar"><div class="stabs" id="stabs"></div><div class="sbody" id="sbody"></div></div>
|
||||
<div id="map-container">
|
||||
<div id="map"></div>
|
||||
<div id="mcp">
|
||||
<div class="mcp-h">Layer Peta</div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#f87171"></div>Penduduk Miskin</div><div class="tog ${lyrs.miskin?'on':''}" id="ly-miskin" onclick="togLyr(this,'miskin')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fbbf24"></div>Penduduk Rentan</div><div class="tog ${lyrs.rentan?'on':''}" id="ly-rentan" onclick="togLyr(this,'rentan')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#34d399"></div>Penduduk Mampu</div><div class="tog ${lyrs.mampu?'on':''}" id="ly-mampu" onclick="togLyr(this,'mampu')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#4f8ef7"></div>Rumah Ibadah</div><div class="tog ${lyrs.ri?'on':''}" id="ly-ri" onclick="togLyr(this,'ri')"></div></div>
|
||||
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fb7185"></div>Laporan Warga</div><div class="tog ${lyrs.laporan?'on':''}" id="ly-lap" onclick="togLyr(this,'laporan')"></div></div>
|
||||
<div class="mcp-div"></div>
|
||||
<div class="rad-lbl"><span>Radius Distribusi</span><span id="rv">${rad} m</span></div>
|
||||
<input type="range" min="100" max="1000" step="50" value="${rad}" id="rsl" oninput="setRad(this.value)">
|
||||
</div>
|
||||
</div>`;
|
||||
if(map){map.remove();map=null;}
|
||||
initMap();rerender();buildSB();
|
||||
}
|
||||
|
||||
function pgPenduduk(){
|
||||
const cW=['superadmin','pengurus','relawan'].includes(cu?.role);
|
||||
document.getElementById('app').innerHTML=`
|
||||
<div class="pwrap">
|
||||
<div class="pinner">
|
||||
<div class="pheader">
|
||||
<div class="ptitle">Data Penduduk <small>${pD.length} terdaftar</small></div>
|
||||
${cW?`<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-success" onclick="openAddP()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah Penduduk</button>
|
||||
<button class="btn btn-primary" onclick="openAddKK()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah KK</button>
|
||||
</div>`:''}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
|
||||
<input type="text" id="qp" placeholder="Cari nama atau NIK..." style="flex:1;min-width:180px;height:34px;padding:0 12px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" oninput="fP()">
|
||||
<select id="fsep" style="height:34px;padding:0 10px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" onchange="fP()"><option value="">Semua Ekonomi</option><option value="miskin">Miskin</option><option value="rentan">Rentan</option><option value="mampu">Mampu</option></select>
|
||||
<select id="fkesp" style="height:34px;padding:0 10px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" onchange="fP()"><option value="">Semua Kesehatan</option><option value="sehat">Sehat</option><option value="sakit_ringan">Sakit Ringan</option><option value="sakit_berat">Sakit Berat</option><option value="disabilitas">Disabilitas</option></select>
|
||||
</div>
|
||||
<div style="background:var(--bg2);border:1px solid var(--b);border-radius:var(--r);overflow:hidden">
|
||||
<div id="ptbl" style="overflow-x:auto">${pTbl(pD)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
function pTbl(d){
|
||||
if(!d.length)return`<div class="empty"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg><p>Tidak ada data</p></div>`;
|
||||
return`<table class="dtable"><thead><tr><th>NIK</th><th>Nama</th><th>JK</th><th>Umur</th><th>Profil</th><th>Ekonomi</th><th>Kesehatan</th><th>Pekerjaan</th><th>Bantuan</th><th></th></tr></thead><tbody>${d.slice(0,100).map(p=>`<tr><td style="font-family:var(--mono);font-size:10px;color:var(--t3)">${p.nik||'—'}</td><td style="font-weight:600">${p.nama}</td><td>${p.jenis_kelamin||'—'}</td><td style="font-family:var(--mono)">${p.umur||'?'}</td><td><span class="badge b-b">${p.profil_umur||'—'}</span></td><td><span class="badge ${p.status_ekonomi==='miskin'?'b-r':p.status_ekonomi==='rentan'?'b-a':'b-g'}">${p.status_ekonomi}</span></td><td style="font-size:11px;color:${p.kondisi_kesehatan==='sehat'?'var(--green)':p.kondisi_kesehatan==='disabilitas'?'var(--rose)':'var(--amber)'}">${p.kondisi_kesehatan||'sehat'}</td><td style="font-size:11px;color:var(--t2)">${p.pekerjaan||'—'}</td><td style="text-align:center;font-family:var(--mono)">${p.total_bantuan||0}x</td><td><button class="btn btn-xs btn-ghost" onclick="detP(${p.id})">Detail</button></td></tr>`).join('')}${d.length>100?`<tr><td colspan="10" style="text-align:center;padding:12px;font-size:11px;color:var(--t3)">Menampilkan 100 dari ${d.length}. Gunakan filter untuk mempersempit pencarian.</td></tr>`:''}</tbody></table>`;
|
||||
}
|
||||
function fP(){const q=(document.getElementById('qp')?.value||'').toLowerCase();const fse=document.getElementById('fsep')?.value||'';const fkes=document.getElementById('fkesp')?.value||'';const f=pD.filter(p=>(!q||p.nama.toLowerCase().includes(q)||(p.nik||'').includes(q))&&(!fse||p.status_ekonomi===fse)&&(!fkes||p.kondisi_kesehatan===fkes));const el=document.getElementById('ptbl');if(el)el.innerHTML=pTbl(f);}
|
||||
|
||||
function pgBantuan(){
|
||||
document.getElementById('app').innerHTML=`
|
||||
<div class="pwrap">
|
||||
<div class="pinner">
|
||||
<div class="pheader"><div class="ptitle">Manajemen Bantuan Sosial</div></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
|
||||
<div class="scard2"><h3>${icoGift} Program Bantuan</h3><div style="display:flex;flex-direction:column;gap:6px">${bD.map(b=>`<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--b);font-size:12px"><span>${b.nama}</span><span class="badge b-b">${b.jenis}</span></div>`).join('')}</div></div>
|
||||
<div class="scard2"><h3>${icoWarn} Belum Terima Bantuan</h3><div id="blist" style="display:flex;flex-direction:column;gap:5px;max-height:240px;overflow-y:auto;font-size:12px"><span style="color:var(--t3)">Memuat...</span></div></div>
|
||||
</div>
|
||||
<div class="scard2">
|
||||
<h3>Catat Penyaluran Bantuan</h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end">
|
||||
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Penduduk</div><select id="hb-pid" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:11px;outline:none"><option value="">-- Pilih --</option>${pD.map(p=>`<option value="${p.id}">${p.nama}</option>`).join('')}</select></div>
|
||||
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Program Bantuan</div><select id="hb-bid" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:11px;outline:none"><option value="">-- Pilih --</option>${bD.map(b=>`<option value="${b.id}">${b.nama}</option>`).join('')}</select></div>
|
||||
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Tanggal</div><input type="date" id="hb-tgl" value="${new Date().toISOString().split('T')[0]}" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none"></div>
|
||||
<button class="btn btn-success" onclick="catatB()">Catat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
rq(EP.b,'GET',null,'action=belum_terima').then(d=>{const el=document.getElementById('blist');if(!el)return;if(!d.length){el.innerHTML=`<div style="color:var(--green);font-size:12px">Semua warga sudah pernah menerima bantuan</div>`;return;}el.innerHTML=d.slice(0,20).map(p=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--b)"><span>${p.nama}</span><span class="badge ${p.status_ekonomi==='miskin'?'b-r':'b-a'}">${p.status_ekonomi}</span></div>`).join('');}).catch(()=>{});
|
||||
}
|
||||
async function catatB(){const pid=document.getElementById('hb-pid')?.value;const bid=document.getElementById('hb-bid')?.value;const tgl=document.getElementById('hb-tgl')?.value;if(!pid||!bid){toast('Pilih penduduk dan program bantuan','err');return;}try{await rq(EP.b,'POST',{penduduk_id:parseInt(pid),bantuan_id:parseInt(bid),tanggal:tgl,status:'disalurkan'},'action=histori');toast('Penyaluran bantuan berhasil dicatat','ok');}catch(e){toast(e.message,'err');}}
|
||||
|
||||
function pgLaporan(){
|
||||
document.getElementById('app').innerHTML=`
|
||||
<div class="pwrap">
|
||||
<div class="pinner">
|
||||
<div class="pheader">
|
||||
<div class="ptitle">Laporan Masyarakat <small>${lapD.filter(l=>l.status!=='selesai').length} aktif</small></div>
|
||||
<button class="btn btn-danger" onclick="openM('lap')"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Buat Laporan</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:7px;margin-bottom:16px;flex-wrap:wrap">${['','pending','diverifikasi','diproses','selesai'].map((s,i)=>`<button class="btn btn-sm ${i===0?'btn-primary':''}" id="fb-${s||'all'}" onclick="fLap('${s}',this)">${s||'Semua'}</button>`).join('')}</div>
|
||||
<div id="lap-list" style="display:flex;flex-direction:column;gap:10px">${lapCards(lapD)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.getElementById('dlap').style.display='none';
|
||||
}
|
||||
function lapCards(d){if(!d.length)return`<div class="empty"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p>Tidak ada laporan</p></div>`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);return d.map(l=>{const bd=l.urgensi==='darurat'?'rgba(251,113,133,.3)':l.urgensi==='tinggi'?'rgba(248,113,113,.2)':'var(--b)';return`<div style="background:var(--bg2);border:1px solid ${bd};border-radius:var(--r);padding:16px"><div style="display:flex;justify-content:space-between;gap:12px;margin-bottom:8px"><div style="font-size:13px;font-weight:600;line-height:1.5;letter-spacing:-.2px">${(l.deskripsi||'').substring(0,90)}${(l.deskripsi||'').length>90?'…':''}</div><div style="display:flex;gap:5px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end"><span class="badge u-${l.urgensi}">${(l.urgensi||'').toUpperCase()}</span><span class="badge b-gray">${l.status}</span></div></div><div style="font-size:11px;color:var(--t3);margin-bottom:10px">${l.alamat_laporan||'—'} · ${l.pelapor_nama||'Anonim'} · ${new Date(l.created_at).toLocaleDateString('id-ID')}</div><div style="display:flex;gap:7px;align-items:center;flex-wrap:wrap"><button class="btn btn-sm" onclick="detLap(${l.id})">Lihat Detail</button>${cW?`<select class="btn btn-sm" style="height:28px;font-size:11px;cursor:pointer" onchange="uLS(${l.id},this.value,this)"><option value="">Update Status</option>${['pending','diverifikasi','diproses','selesai'].map(s=>`<option value="${s}"${l.status===s?' selected':''}>${s}</option>`).join('')}</select>`:''}</div></div>`;}).join('');}
|
||||
async function fLap(s,btn){document.querySelectorAll('[id^="fb-"]').forEach(b=>b.classList.remove('btn-primary'));btn?.classList.add('btn-primary');try{const d=await rq(EP.lap,'GET',null,s?'status='+s:'');const el=document.getElementById('lap-list');if(el)el.innerHTML=lapCards(d);}catch(e){toast(e.message,'err');}}
|
||||
async function uLS(id,s,sel){if(!s)return;try{await rq(EP.lap,'PUT',{status:s},'id='+id);const i=lapD.findIndex(l=>l.id===id);if(i>-1)lapD[i].status=s;toast('Status diperbarui','ok');}catch(e){toast(e.message,'err');if(sel)sel.value='';}}
|
||||
|
||||
function pgChat(){
|
||||
document.getElementById('app').innerHTML=`
|
||||
<div class="pwrap">
|
||||
<div class="pinner" style="max-width:680px">
|
||||
<div class="pheader"><div class="ptitle">Koordinasi Pengurus</div><span style="font-size:11px;color:var(--t3)">Pesan dapat dilihat semua pengurus & relawan</span></div>
|
||||
<div style="background:var(--bg2);border:1px solid var(--b2);border-radius:var(--r);padding:18px">
|
||||
<div class="chat-msgs" id="cmsgs"></div>
|
||||
<div class="cinp-row">
|
||||
<textarea class="cinp" id="cinp" placeholder="Tulis pesan... (Enter untuk kirim)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat();}"></textarea>
|
||||
<button class="btn btn-primary btn-sm" onclick="sendChat()" style="height:38px;padding:0 14px">Kirim</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
loadChat();document.getElementById('dchat').style.display='none';
|
||||
}
|
||||
async function loadChat(){try{const msgs=await rq(EP.chat,'GET',null,'since='+lastC);if(!msgs.length)return;lastC=msgs[msgs.length-1].created_at;const el=document.getElementById('cmsgs');if(!el)return;msgs.forEach(m=>{const mine=m.dari_id===cu.id;const d=document.createElement('div');d.className='cmsg '+(mine?'mine':'other');d.innerHTML=`<div class="cmsg-meta">${mine?'Anda':m.dari_nama}${m.dari_ri?' · '+m.dari_ri:''}</div><div class="cmsg-bubble">${m.pesan}</div><div class="cmsg-time">${new Date(m.created_at).toLocaleTimeString('id',{hour:'2-digit',minute:'2-digit'})}</div>`;el.appendChild(d);});el.scrollTop=el.scrollHeight;}catch(e){}}
|
||||
async function pollChat(){if(curPg==='chat'){loadChat();return;}try{const msgs=await rq(EP.chat,'GET',null,'since='+lastC);if(msgs.length){lastC=msgs[msgs.length-1].created_at;document.getElementById('dchat').style.display='inline-block';}}catch(e){}}
|
||||
async function sendChat(){const inp=document.getElementById('cinp');const p=inp?.value.trim();if(!p)return;try{await rq(EP.chat,'POST',{pesan:p});inp.value='';loadChat();}catch(e){toast(e.message,'err');}}
|
||||
|
||||
// DETAILS
|
||||
async function detRI(id){ld(true);try{const ri=await rq(EP.ri,'GET',null,'id='+id);const kk2=kkD.filter(k=>k.rumah_ibadah_id==id);document.getElementById('det-h').innerHTML=`<div class="mico mi-b">${riSVG(ri.jenis)}</div>${ri.nama}`;document.getElementById('det-b').innerHTML=`<span class="badge b-b" style="margin-bottom:14px;display:inline-block">${ri.jenis}</span><table class="dtbl"><tr><td>Kontak</td><td>${ri.kontak||'—'}</td></tr><tr><td>Alamat</td><td>${ri.alamat||'—'}</td></tr><tr><td>Kelurahan</td><td>${ri.kelurahan||'—'}</td></tr><tr><td>Jumlah KK</td><td>${ri.jumlah_kk||0} KK (${ri.jumlah_jiwa||0} jiwa)</td></tr><tr><td>KK Miskin</td><td style="color:var(--red);font-weight:600">${ri.kk_miskin||0} KK</td></tr></table>${kk2.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">KK Terdampak (${kk2.length})</div><div class="dklist">${kk2.map(k=>`<div class="dkrow"><span>${k.nama_kk}</span><span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}">${k.status_ekonomi}</span></div>`).join('')}</div>`:''}`;document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
async function detP(id){ld(true);try{const p=await rq(EP.p,'GET',null,'id='+id);const col=EC[p.status_ekonomi]||'#888';document.getElementById('det-h').innerHTML=`<div class="mico mi-g"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>${p.nama}`;document.getElementById('det-b').innerHTML=`<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px"><span class="badge ${p.status_ekonomi==='miskin'?'b-r':p.status_ekonomi==='rentan'?'b-a':'b-g'}">${(p.status_ekonomi||'').toUpperCase()}</span><span class="badge b-b">${p.profil_umur||'—'}</span><span class="badge ${p.kondisi_kesehatan==='sehat'?'b-g':'b-r'}">${p.kondisi_kesehatan||'sehat'}</span></div>${p.peringatan_pendidikan?`<div class="wbox" style="display:block;margin-bottom:14px">${p.peringatan_pendidikan}</div>`:''}<table class="dtbl"><tr><td>NIK</td><td style="font-family:var(--mono)">${p.nik||'—'}</td></tr><tr><td>Jenis Kelamin</td><td>${p.jenis_kelamin==='L'?'Laki-laki':'Perempuan'}</td></tr><tr><td>Tanggal Lahir</td><td>${p.tanggal_lahir||'—'} (${p.umur||'?'} tahun)</td></tr><tr><td>Status Keluarga</td><td>${(p.status_keluarga||'—').replace('_',' ')}</td></tr><tr><td>Status Perkawinan</td><td>${(p.status_perkawinan||'—').replace(/_/g,' ')}</td></tr><tr><td>Pekerjaan</td><td>${p.pekerjaan||'—'}</td></tr><tr><td>Penghasilan</td><td>Rp ${Number(p.penghasilan||0).toLocaleString('id-ID')}/bulan</td></tr><tr><td>Pendidikan</td><td>${(p.pendidikan_terakhir||'—').toUpperCase()} · ${(p.status_pendidikan||'—').replace('_',' ')}</td></tr><tr><td>No. KK</td><td style="font-family:var(--mono)">${p.no_kk||'—'}</td></tr><tr><td>Alamat</td><td>${p.alamat_kk||'—'}${p.rt?' RT '+p.rt+'/RW '+p.rw:''}</td></tr><tr><td>Kelurahan</td><td>${p.kelurahan||'—'}, ${p.kecamatan||'—'}</td></tr></table>${p.histori_bantuan?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Histori Bantuan (${p.histori_bantuan.length}x)</div><div class="dklist">${p.histori_bantuan.map(h=>`<div class="dkrow"><span>${h.bantuan_nama}</span><div style="display:flex;align-items:center;gap:8px"><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${h.tanggal}</span><span class="badge ${h.status==='disalurkan'?'b-g':'b-a'}">${h.status}</span></div></div>`).join('')}</div>`:`<div style="font-size:12px;padding:10px;background:var(--rdim);border-radius:var(--rs);margin-top:14px;color:var(--red)">Belum pernah menerima bantuan apapun</div>`}${p.riwayat_pelatihan?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Riwayat Pelatihan</div><div class="dklist">${p.riwayat_pelatihan.map(r=>`<div class="dkrow"><span>${r.nama_pelatihan}</span><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${r.tanggal_ikut}</span></div>`).join('')}</div>`:''}`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>${cW?`<button class="btn" onclick="closeM('det');openEditP(${p.id})">Edit Data</button>`:''}${cu?.role==='superadmin'?`<button class="btn btn-danger" onclick="delP(${p.id})">Hapus</button>`:''}`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
async function detKK(id){ld(true);try{const k=await rq(EP.kk,'GET',null,'id='+id);document.getElementById('det-h').innerHTML=`<div class="mico mi-b">${icoHome}</div>${k.nama_kk}`;document.getElementById('det-b').innerHTML=`<span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}" style="margin-bottom:14px;display:inline-block">${(k.status_ekonomi||'').toUpperCase()}</span><table class="dtbl"><tr><td>No. KK</td><td style="font-family:var(--mono)">${k.no_kk||'—'}</td></tr><tr><td>Alamat</td><td>${k.alamat||'—'} RT ${k.rt||'-'}/RW ${k.rw||'-'}</td></tr><tr><td>Kelurahan</td><td>${k.kelurahan||'—'}, ${k.kecamatan||'—'}</td></tr><tr><td>Rumah Ibadah</td><td>${k.ri_nama||'—'}</td></tr></table>${k.anggota?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Anggota (${k.anggota.length} jiwa)</div><div class="dklist">${k.anggota.map(a=>`<div class="dkrow"><span><strong>${a.nama}</strong> · ${(a.status_keluarga||'').replace('_',' ')} · ${a.umur}thn</span><span class="badge ${a.kondisi_kesehatan==='sehat'?'b-g':a.kondisi_kesehatan==='disabilitas'?'b-ro':'b-a'}">${a.kondisi_kesehatan||'sehat'}</span></div>`).join('')}</div>`:''}`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>${cW?`<button class="btn btn-success" onclick="closeM('det');openAddP(${id})">+ Tambah Anggota</button>`:''}`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
async function detLap(id){const l=lapD.find(x=>x.id===id);if(!l)return;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-h').innerHTML=`<div class="mico mi-r">${icoWarn}</div>Laporan Masyarakat`;document.getElementById('det-b').innerHTML=`<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px"><span class="badge u-${l.urgensi}">${(l.urgensi||'').toUpperCase()}</span><span class="badge b-gray">${l.status}</span></div><table class="dtbl"><tr><td>Deskripsi</td><td style="line-height:1.7">${l.deskripsi||'—'}</td></tr><tr><td>Pelapor</td><td>${l.pelapor_nama||'Anonim'}${l.pelapor_telepon?' ('+l.pelapor_telepon+')':''}</td></tr><tr><td>Lokasi</td><td>${l.alamat_laporan||'—'}</td></tr><tr><td>Tanggal</td><td>${new Date(l.created_at).toLocaleDateString('id-ID',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</td></tr></table>${l.foto?`<img src="${l.foto}" style="width:100%;max-height:200px;object-fit:cover;border-radius:var(--rs);margin-top:14px">`:''}${cW?`<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--b)"><div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin-bottom:8px">Update Status</div><div style="display:flex;gap:7px;flex-wrap:wrap">${['diverifikasi','diproses','selesai'].map(s=>`<button class="btn btn-sm ${l.status===s?'btn-primary':''}" onclick="uLS(${l.id},'${s}',null);closeM('det');toast('Status diperbarui','ok')">${s}</button>`).join('')}</div></div>`:''}`;document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>`;openM('det');}
|
||||
|
||||
// FORMS
|
||||
function openAddKK(){eKK=null;document.getElementById('mkk-t').textContent='Tambah Kartu Keluarga';['kk-nokk','kk-nama','kk-alamat','kk-rt','kk-rw','kk-kel','kk-kec','kk-lat','kk-lng'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});document.getElementById('kk-se').value='rentan';const sel=document.getElementById('kk-ri');if(sel)sel.innerHTML='<option value="">-- Pilih --</option>'+riD.map(r=>`<option value="${r.id}">${r.nama}</option>`).join('');openM('kk');}
|
||||
async function saveKK(){const no=document.getElementById('kk-nokk')?.value.trim(),nm=document.getElementById('kk-nama')?.value.trim();if(!no||!nm){toast('No KK dan nama wajib diisi','err');return;}if(no.length!==16){toast('No KK harus 16 digit','err');return;}const body={no_kk:no,nama_kk:nm,alamat:document.getElementById('kk-alamat')?.value||'',rt:document.getElementById('kk-rt')?.value||'',rw:document.getElementById('kk-rw')?.value||'',kelurahan:document.getElementById('kk-kel')?.value||'',kecamatan:document.getElementById('kk-kec')?.value||'',lat:parseFloat(document.getElementById('kk-lat')?.value)||null,lng:parseFloat(document.getElementById('kk-lng')?.value)||null,rumah_ibadah_id:document.getElementById('kk-ri')?.value||null,status_ekonomi:document.getElementById('kk-se')?.value||'rentan'};ld(true);try{if(eKK){await rq(EP.kk,'PUT',body,'id='+eKK);eKK=null;toast('KK diperbarui','ok');}else{await rq(EP.kk,'POST',body);toast('KK berhasil ditambahkan','ok');}closeM('kk');const kk=await rq(EP.kk);kkD=kk||[];fKK();rDash();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
function openAddP(kkId=null){eP=null;document.getElementById('mp-t').textContent='Tambah Data Penduduk';document.getElementById('p-nik').readOnly=false;['p-nik','p-nama','p-tgl','p-kerja','p-gaji','p-cat'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});['p-jk','p-sk','p-sp','p-sh','p-se','p-pend','p-spend','p-kes'].forEach(id=>{const el=document.getElementById(id);if(!el)return;if(id==='p-sh')el.value='hidup';else if(id==='p-se')el.value='rentan';else if(id==='p-pend')el.value='tidak_sekolah';else if(id==='p-spend')el.value='tidak_sekolah';else if(id==='p-kes')el.value='sehat';else el.value=el.options[0]?.value;});document.getElementById('wp').style.display='none';const sel=document.getElementById('p-kk');if(sel)sel.innerHTML='<option value="">-- Pilih KK --</option>'+kkD.map(k=>`<option value="${k.id}"${k.id==kkId?' selected':''}>${k.nama_kk} (${k.no_kk})</option>`).join('');openM('p');}
|
||||
|
||||
async function openEditP(id){const p=pD.find(x=>x.id===id);if(!p){toast('Data tidak ditemukan','err');return;}eP=id;document.getElementById('mp-t').textContent='Edit Data Penduduk';document.getElementById('p-nik').value=p.nik||'';document.getElementById('p-nik').readOnly=true;document.getElementById('p-nama').value=p.nama||'';document.getElementById('p-tgl').value=p.tanggal_lahir||'';document.getElementById('p-jk').value=p.jenis_kelamin||'L';document.getElementById('p-sk').value=p.status_keluarga||'kepala';document.getElementById('p-sp').value=p.status_perkawinan||'belum_kawin';document.getElementById('p-sh').value=p.status_hidup||'hidup';document.getElementById('p-se').value=p.status_ekonomi||'rentan';document.getElementById('p-pend').value=p.pendidikan_terakhir||'tidak_sekolah';document.getElementById('p-spend').value=p.status_pendidikan||'tidak_sekolah';document.getElementById('p-kes').value=p.kondisi_kesehatan||'sehat';document.getElementById('p-kerja').value=p.pekerjaan||'';document.getElementById('p-gaji').value=p.penghasilan||0;document.getElementById('p-cat').value=p.catatan||'';const sel=document.getElementById('p-kk');if(sel)sel.innerHTML='<option value="">-- Pilih KK --</option>'+kkD.map(k=>`<option value="${k.id}"${k.id==p.keluarga_id?' selected':''}>${k.nama_kk} (${k.no_kk})</option>`).join('');cekPend();openM('p');}
|
||||
|
||||
async function saveP(){const nm=document.getElementById('p-nama')?.value.trim(),tgl=document.getElementById('p-tgl')?.value,kkid=document.getElementById('p-kk')?.value,nik=document.getElementById('p-nik')?.value.trim();if(!nm||!tgl||!kkid){toast('Nama, tanggal lahir, dan KK wajib diisi','err');return;}if(!eP&&(!nik||nik.length!==16)){toast('NIK harus 16 digit','err');return;}const body={nik,nama:nm,tanggal_lahir:tgl,jenis_kelamin:document.getElementById('p-jk')?.value||'L',status_keluarga:document.getElementById('p-sk')?.value||'kepala',status_perkawinan:document.getElementById('p-sp')?.value||'belum_kawin',status_hidup:document.getElementById('p-sh')?.value||'hidup',status_ekonomi:document.getElementById('p-se')?.value||'rentan',pendidikan_terakhir:document.getElementById('p-pend')?.value||'tidak_sekolah',status_pendidikan:document.getElementById('p-spend')?.value||'tidak_sekolah',kondisi_kesehatan:document.getElementById('p-kes')?.value||'sehat',pekerjaan:document.getElementById('p-kerja')?.value||'',penghasilan:parseFloat(document.getElementById('p-gaji')?.value||0),keluarga_id:parseInt(kkid),catatan:document.getElementById('p-cat')?.value||''};ld(true);try{let res;if(eP){res=await rq(EP.p,'PUT',body,'id='+eP);eP=null;toast('Data penduduk diperbarui','ok');}else{res=await rq(EP.p,'POST',body);toast('Penduduk berhasil ditambahkan','ok');}if(res?.peringatan)setTimeout(()=>toast('Perhatian: '+res.peringatan,'warn'),500);closeM('p');const pd=await rq(EP.p);pD=pd||[];rerender();rSB();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
async function delP(id){if(!confirm('Hapus data penduduk ini secara permanen?'))return;ld(true);try{await rq(EP.p,'DELETE',null,'id='+id);pD=pD.filter(p=>p.id!==id);closeM('det');rerender();rSB();toast('Data dihapus','ok');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
function cekPend(){const tgl=document.getElementById('p-tgl')?.value;if(!tgl)return;const umur=Math.floor((new Date()-new Date(tgl))/31557600000);const pend=document.getElementById('p-pend')?.value;const spend=document.getElementById('p-spend')?.value;const el=document.getElementById('wp');if(!el)return;let w='';if(umur>=7&&umur<=15&&spend==='tidak_sekolah'&&['tidak_sekolah','sd'].includes(pend))w=`Usia ${umur} tahun seharusnya masih bersekolah SD/SMP.`;else if(umur>=16&&umur<=18&&spend==='tidak_sekolah')w=`Usia ${umur} tahun seharusnya masih bersekolah SMA/SMK.`;el.style.display=w?'block':'none';el.textContent=w?'Perhatian: '+w:'';}
|
||||
|
||||
async function saveLap(){const desc=document.getElementById('lap-desc')?.value.trim();if(!desc){toast('Deskripsi laporan wajib diisi','err');return;}const fd=new FormData();fd.append('deskripsi',desc);fd.append('pelapor_nama',document.getElementById('lap-nama')?.value||'');fd.append('pelapor_telepon',document.getElementById('lap-telp')?.value||'');fd.append('alamat_laporan',document.getElementById('lap-alamat')?.value||'');fd.append('urgensi',document.getElementById('lap-urg')?.value||'sedang');const lat=document.getElementById('lap-lat')?.value;const lng=document.getElementById('lap-lng')?.value;if(lat)fd.append('lat',lat);if(lng)fd.append('lng',lng);const foto=document.getElementById('lap-foto')?.files?.[0];if(foto)fd.append('foto',foto);ld(true);try{const r=await fetch(EP.lap,{method:'POST',body:fd});const d=await r.json();if(!d.success)throw new Error(d.message);toast('Laporan berhasil dikirim. Terima kasih!','ok');closeM('lap');const lp=await rq(EP.lap);lapD=lp||[];rerender();checkAlerts();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
||||
|
||||
function checkAlerts(){const dar=lapD.filter(l=>l.urgensi==='darurat'&&l.status!=='selesai');document.getElementById('dlap').style.display=dar.length?'inline-block':'none';}
|
||||
|
||||
// MODAL
|
||||
function openM(n){document.getElementById('modal-'+n)?.classList.add('open');}
|
||||
function closeM(n){document.getElementById('modal-'+n)?.classList.remove('open');}
|
||||
document.querySelectorAll('.overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)closeM(o.id.replace('modal-',''));}));
|
||||
|
||||
function toggleSB(){document.getElementById('sidebar')?.classList.toggle('off');}
|
||||
|
||||
document.getElementById('lp')?.addEventListener('keydown',e=>{if(e.key==='Enter')doLogin();});
|
||||
document.getElementById('lu')?.addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('lp')?.focus();});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
# Block semua eksekusi PHP di folder uploads
|
||||
<FilesMatch "\.php$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
Options -Indexes
|
||||
+651
@@ -0,0 +1,651 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Portal WebGIS Terpadu</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #080b10;
|
||||
--metal-1: #111820;
|
||||
--metal-2: #1c252e;
|
||||
--metal-3: #2d3740;
|
||||
--text: #eef6f8;
|
||||
--muted: #a5b3ba;
|
||||
--line: rgba(183, 205, 215, 0.18);
|
||||
--primary: #55d7e8;
|
||||
--primary-dark: #82f1ff;
|
||||
--accent: #d7ad62;
|
||||
--blue: #66a8ff;
|
||||
--red: #ff716d;
|
||||
--purple: #b48cff;
|
||||
--shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at 16% 14%, rgba(85, 215, 232, 0.22), transparent 24%),
|
||||
radial-gradient(circle at 86% 18%, rgba(215, 173, 98, 0.14), transparent 25%),
|
||||
radial-gradient(circle at 50% 100%, rgba(102, 168, 255, 0.13), transparent 34%),
|
||||
linear-gradient(145deg, #080b10 0%, #111922 48%, #06080c 100%);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background-image:
|
||||
linear-gradient(rgba(183, 205, 215, 0.055) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(183, 205, 215, 0.055) 1px, transparent 1px);
|
||||
background-size: 46px 46px;
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.92), transparent 86%);
|
||||
animation: gridDrift 18s linear infinite;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
linear-gradient(110deg, transparent 0 35%, rgba(255, 255, 255, 0.055) 45%, transparent 56%),
|
||||
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.025) 0 1px, transparent 1px 5px);
|
||||
opacity: 0.62;
|
||||
pointer-events: none;
|
||||
animation: scanSweep 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.future-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.future-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -20%;
|
||||
background:
|
||||
radial-gradient(circle, rgba(85, 215, 232, 0.9) 0 2px, transparent 3px),
|
||||
radial-gradient(circle, rgba(215, 173, 98, 0.72) 0 1px, transparent 2px),
|
||||
radial-gradient(circle, rgba(180, 140, 255, 0.8) 0 1px, transparent 2px);
|
||||
background-size: 180px 180px, 260px 260px, 340px 340px;
|
||||
background-position: 0 0, 80px 120px, 180px 40px;
|
||||
filter: drop-shadow(0 0 8px rgba(85, 215, 232, 0.4));
|
||||
opacity: 0.42;
|
||||
animation: starField 28s linear infinite;
|
||||
}
|
||||
|
||||
.future-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 8%, rgba(85, 215, 232, 0.24) 9%, transparent 10% 42%, rgba(215, 173, 98, 0.18) 43%, transparent 44%),
|
||||
linear-gradient(135deg, transparent 0 46%, rgba(102, 168, 255, 0.16) 47%, transparent 49%),
|
||||
linear-gradient(45deg, transparent 0 54%, rgba(180, 140, 255, 0.13) 55%, transparent 57%);
|
||||
background-size: 520px 100%, 420px 420px, 620px 620px;
|
||||
opacity: 0.55;
|
||||
transform: perspective(900px) rotateX(58deg) translateY(22%);
|
||||
transform-origin: bottom;
|
||||
animation: dataHighway 9s linear infinite;
|
||||
}
|
||||
|
||||
.energy-ring {
|
||||
position: absolute;
|
||||
width: 44vmax;
|
||||
height: 44vmax;
|
||||
left: 50%;
|
||||
top: 48%;
|
||||
border: 1px solid rgba(85, 215, 232, 0.16);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 70px rgba(85, 215, 232, 0.12),
|
||||
inset 0 0 70px rgba(102, 168, 255, 0.08);
|
||||
transform: translate(-50%, -50%);
|
||||
animation: haloRotate 24s linear infinite;
|
||||
}
|
||||
|
||||
.energy-ring::before,
|
||||
.energy-ring::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 12%;
|
||||
border-radius: 50%;
|
||||
border: 1px dashed rgba(215, 173, 98, 0.2);
|
||||
}
|
||||
|
||||
.energy-ring::after {
|
||||
inset: 25%;
|
||||
border-color: rgba(180, 140, 255, 0.18);
|
||||
animation: innerHaloRotate 14s linear infinite reverse;
|
||||
}
|
||||
|
||||
.beam {
|
||||
position: absolute;
|
||||
left: var(--x);
|
||||
top: -22%;
|
||||
width: 2px;
|
||||
height: 44%;
|
||||
background: linear-gradient(to bottom, transparent, var(--beam-color), transparent);
|
||||
box-shadow: 0 0 18px var(--beam-color);
|
||||
opacity: 0;
|
||||
transform: rotate(24deg);
|
||||
animation: lightBeam var(--duration) linear infinite;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(1120px, calc(100% - 32px));
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 50px 0 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 34px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 28px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: fit-content;
|
||||
margin-bottom: 16px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(85, 215, 232, 0.28);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.035));
|
||||
color: var(--primary-dark);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 0 24px rgba(85, 215, 232, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(34px, 5vw, 62px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 34px rgba(85, 215, 232, 0.22);
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 650px;
|
||||
margin: 18px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 18px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
position: relative;
|
||||
min-height: 235px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.035)),
|
||||
linear-gradient(35deg, transparent 44%, rgba(85, 215, 232, 0.28) 45%, rgba(85, 215, 232, 0.28) 47%, transparent 48%),
|
||||
linear-gradient(130deg, transparent 48%, rgba(215, 173, 98, 0.24) 49%, rgba(215, 173, 98, 0.24) 51%, transparent 52%),
|
||||
linear-gradient(160deg, #18212a, #0c1117);
|
||||
box-shadow: var(--shadow);
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.map-panel::before,
|
||||
.map-panel::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
background: var(--primary);
|
||||
box-shadow:
|
||||
72px 48px 0 var(--accent),
|
||||
146px 112px 0 var(--blue),
|
||||
216px 70px 0 var(--red),
|
||||
96px 154px 0 var(--purple);
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
left: 38px;
|
||||
top: 42px;
|
||||
filter: drop-shadow(0 0 10px currentColor);
|
||||
animation: nodePulse 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.map-panel::after {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
left: auto;
|
||||
right: -42px;
|
||||
top: -34px;
|
||||
border: 26px solid rgba(85, 215, 232, 0.12);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
animation: orbitRing 9s linear infinite;
|
||||
}
|
||||
|
||||
.map-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(30deg, transparent 0 47%, rgba(215, 173, 98, 0.42) 48% 50%, transparent 51%),
|
||||
linear-gradient(155deg, transparent 0 49%, rgba(85, 215, 232, 0.22) 50% 52%, transparent 53%);
|
||||
background-size: 180px 120px, 220px 160px;
|
||||
animation: routeFlow 12s linear infinite;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
grid-column: span 2;
|
||||
position: relative;
|
||||
min-height: 218px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(150deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.035) 48%, rgba(255, 255, 255, 0.07)),
|
||||
linear-gradient(145deg, #151d25, #0c1118);
|
||||
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.13);
|
||||
transition: transform 220ms ease, box-shadow 220ms ease, border-color 220ms ease, background 220ms ease;
|
||||
}
|
||||
|
||||
.card:nth-child(4),
|
||||
.card:nth-child(5) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-top: 4px solid var(--card-color);
|
||||
background:
|
||||
linear-gradient(110deg, transparent 0 28%, rgba(255, 255, 255, 0.16) 42%, transparent 56%),
|
||||
radial-gradient(circle at 86% 18%, var(--card-soft), transparent 34%);
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
transform: translateX(-38%);
|
||||
transition: transform 520ms ease;
|
||||
}
|
||||
|
||||
.card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus-visible {
|
||||
transform: translateY(-7px);
|
||||
border-color: color-mix(in srgb, var(--card-color), white 34%);
|
||||
box-shadow: 0 26px 58px rgba(0, 0, 0, 0.45), 0 0 34px color-mix(in srgb, var(--card-color), transparent 64%);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.card:hover::before,
|
||||
.card:focus-visible::before {
|
||||
transform: translateX(38%);
|
||||
}
|
||||
|
||||
.card-top,
|
||||
.card-bottom {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 8px;
|
||||
color: var(--card-color);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.035)),
|
||||
var(--card-soft);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 20px color-mix(in srgb, var(--card-color), transparent 72%);
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 18px 0 10px;
|
||||
font-size: 22px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0;
|
||||
color: #f6fbfc;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
color: var(--primary-dark);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(145deg, var(--card-color), color-mix(in srgb, var(--card-color), black 32%));
|
||||
color: #071014;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease;
|
||||
flex: 0 0 auto;
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--card-color), transparent 55%);
|
||||
}
|
||||
|
||||
.card:hover .arrow,
|
||||
.card:focus-visible .arrow {
|
||||
transform: translateX(3px) rotate(-12deg);
|
||||
}
|
||||
|
||||
footer {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes gridDrift {
|
||||
from { background-position: 0 0, 0 0; }
|
||||
to { background-position: 46px 46px, 46px 46px; }
|
||||
}
|
||||
|
||||
@keyframes scanSweep {
|
||||
0%, 100% { transform: translateX(-18%); opacity: 0.4; }
|
||||
50% { transform: translateX(18%); opacity: 0.72; }
|
||||
}
|
||||
|
||||
@keyframes routeFlow {
|
||||
from { background-position: 0 0, 0 0; }
|
||||
to { background-position: 180px 120px, -220px 160px; }
|
||||
}
|
||||
|
||||
@keyframes nodePulse {
|
||||
0%, 100% { opacity: 0.68; transform: scale(0.92); }
|
||||
50% { opacity: 1; transform: scale(1.16); }
|
||||
}
|
||||
|
||||
@keyframes orbitRing {
|
||||
from { transform: rotate(0deg) scale(1); }
|
||||
50% { transform: rotate(180deg) scale(1.08); }
|
||||
to { transform: rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes starField {
|
||||
from { transform: translate3d(0, 0, 0) rotate(0deg); }
|
||||
to { transform: translate3d(-180px, 180px, 0) rotate(4deg); }
|
||||
}
|
||||
|
||||
@keyframes dataHighway {
|
||||
from {
|
||||
background-position: 0 0, 0 0, 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 520px 0, 420px 420px, -620px 620px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes haloRotate {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes innerHaloRotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes lightBeam {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(-12vw, -20vh, 0) rotate(24deg);
|
||||
}
|
||||
12% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
48% {
|
||||
opacity: 0.18;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(24vw, 140vh, 0) rotate(24deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.page {
|
||||
padding-top: 34px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
min-height: 185px;
|
||||
}
|
||||
|
||||
.card,
|
||||
.card:nth-child(4),
|
||||
.card:nth-child(5) {
|
||||
grid-column: span 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page {
|
||||
width: min(100% - 24px, 1120px);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card,
|
||||
.card:nth-child(4),
|
||||
.card:nth-child(5) {
|
||||
grid-column: auto;
|
||||
min-height: 190px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="future-bg" aria-hidden="true">
|
||||
<div class="energy-ring"></div>
|
||||
<span class="beam" style="--x: 8%; --duration: 8s; --delay: -1s; --beam-color: rgba(85, 215, 232, 0.72);"></span>
|
||||
<span class="beam" style="--x: 34%; --duration: 11s; --delay: -5s; --beam-color: rgba(215, 173, 98, 0.52);"></span>
|
||||
<span class="beam" style="--x: 61%; --duration: 9s; --delay: -3s; --beam-color: rgba(102, 168, 255, 0.58);"></span>
|
||||
<span class="beam" style="--x: 86%; --duration: 12s; --delay: -7s; --beam-color: rgba(180, 140, 255, 0.5);"></span>
|
||||
</div>
|
||||
<main class="page">
|
||||
<section class="hero" aria-labelledby="page-title">
|
||||
<div>
|
||||
<div class="eyebrow">
|
||||
<span aria-hidden="true">GIS</span>
|
||||
<span>5 aplikasi peta interaktif</span>
|
||||
</div>
|
||||
<h1 id="page-title">Portal WebGIS Terpadu</h1>
|
||||
<p class="lead">Pilih salah satu aplikasi WebGIS untuk membuka peta interaktif.</p>
|
||||
</div>
|
||||
<div class="map-panel" aria-hidden="true">
|
||||
<div class="map-lines"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid" aria-label="Daftar aplikasi WebGIS">
|
||||
<a class="card" href="01/index.html" style="--card-color: #55d7e8; --card-soft: rgba(85, 215, 232, 0.16);">
|
||||
<div class="card-top">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"></path>
|
||||
<path d="M4 11h11"></path>
|
||||
<path d="M17 8h1.5l1.5 2v7a2 2 0 0 0 4 0v-6"></path>
|
||||
<path d="M8 21h3"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h2>Webgis SPBU Point</h2>
|
||||
<p class="meta">Peta sebaran titik SPBU untuk melihat lokasi fasilitas secara cepat.</p>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<span>Buka project</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="02/index.html" style="--card-color: #66a8ff; --card-soft: rgba(102, 168, 255, 0.16);">
|
||||
<div class="card-top">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M5 20c3-4 3-12 6-16"></path>
|
||||
<path d="M19 20c-3-4-3-12-6-16"></path>
|
||||
<path d="M12 4v4"></path>
|
||||
<path d="M12 12v4"></path>
|
||||
<path d="M3 20h18"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h2>Webgis Jalan Parsil</h2>
|
||||
<p class="meta">Visualisasi data jalan dan persil dalam satu tampilan spasial.</p>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<span>Buka project</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="03/index.html" style="--card-color: #d7ad62; --card-soft: rgba(215, 173, 98, 0.16);">
|
||||
<div class="card-top">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M9 18 3 21V6l6-3 6 3 6-3v15l-6 3-6-3Z"></path>
|
||||
<path d="M9 3v15"></path>
|
||||
<path d="M15 6v15"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h2>Webgis Chropleth</h2>
|
||||
<p class="meta">Peta tematik choropleth untuk membaca pola wilayah berbasis warna.</p>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<span>Buka project</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="04/index.html" style="--card-color: #ff716d; --card-soft: rgba(255, 113, 109, 0.15);">
|
||||
<div class="card-top">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 21s7-4.4 7-11a7 7 0 1 0-14 0c0 6.6 7 11 7 11Z"></path>
|
||||
<circle cx="12" cy="10" r="2.5"></circle>
|
||||
<path d="M16 5.5h2.5V3"></path>
|
||||
<path d="M18.5 5.5 15.5 2.5"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h2>Webgis SPBU Upgrade</h2>
|
||||
<p class="meta">Versi pengembangan peta SPBU dengan tampilan dan data yang diperbarui.</p>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<span>Buka project</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="card" href="05/index.php" style="--card-color: #b48cff; --card-soft: rgba(180, 140, 255, 0.15);">
|
||||
<div class="card-top">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 19V5"></path>
|
||||
<path d="M4 19h17"></path>
|
||||
<rect x="7" y="11" width="3" height="5"></rect>
|
||||
<rect x="12" y="8" width="3" height="8"></rect>
|
||||
<rect x="17" y="4" width="3" height="12"></rect>
|
||||
</svg>
|
||||
</span>
|
||||
<h2>Webgis Kemiskinan</h2>
|
||||
<p class="meta">Peta dan data wilayah untuk mendukung pembacaan indikator kemiskinan.</p>
|
||||
</div>
|
||||
<div class="card-bottom">
|
||||
<span>Buka project</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
WebGis by: Muhammad Taufiq Fauzan--D1041231023--Informatika UNTAN
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,78 @@
|
||||
<!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 UAS</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #17212b;
|
||||
background: #f3f6f8;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(920px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 42px 0;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
line-height: 1.2;
|
||||
color: #0e1720;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 18px 0;
|
||||
padding: 22px;
|
||||
border: 1px solid #dce4ea;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
background: #edf2f5;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 22px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Manual Book WebGIS Project UAS</h1>
|
||||
<p>Dokumen ini berisi ringkasan struktur project, akses aplikasi, dan konfigurasi database untuk deployment.</p>
|
||||
|
||||
<section>
|
||||
<h2>Struktur Aplikasi</h2>
|
||||
<ul>
|
||||
<li><code>01</code> - Webgis SPBU Point</li>
|
||||
<li><code>02</code> - Webgis Jalan Parsil</li>
|
||||
<li><code>03</code> - Webgis Chropleth</li>
|
||||
<li><code>04</code> - Webgis SPBU Upgrade</li>
|
||||
<li><code>05</code> - Webgis Kemiskinan</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Akses Lokal</h2>
|
||||
<p>Buka portal utama melalui <code>http://localhost/combinewebgis/</code>, lalu pilih aplikasi WebGIS dari card yang tersedia.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Database</h2>
|
||||
<p>Aplikasi PHP membaca konfigurasi database dari environment variable <code>DB_HOST</code>, <code>DB_USER</code>, <code>DB_PASSWORD</code>, dan <code>DB_NAME</code>. Jika variable belum tersedia, aplikasi memakai fallback lokal XAMPP.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Deployment Docker</h2>
|
||||
<p>Setiap folder aplikasi memiliki <code>Dockerfile</code> sendiri agar bisa dideploy sebagai service terpisah di Coolify.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user