first commit
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/auth.php — Login / Logout / Session check
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = getParam('action', 'login');
|
||||
|
||||
if ($method === 'POST' && $action === 'login') {
|
||||
$data = body();
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
if (!$username || !$password) jsonError('Username dan password wajib diisi.');
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND aktif = 1 LIMIT 1");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
jsonError('Username atau password salah.', 401);
|
||||
}
|
||||
|
||||
$_SESSION['user'] = [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'jabatan' => $user['jabatan'],
|
||||
'can_edit' => (bool)$user['can_edit'],
|
||||
'is_admin' => (bool)$user['is_admin'],
|
||||
'is_wali' => (bool)$user['is_wali'],
|
||||
];
|
||||
|
||||
jsonOk($_SESSION['user'], 'Login berhasil');
|
||||
}
|
||||
|
||||
if ($method === 'POST' && $action === 'logout') {
|
||||
session_destroy();
|
||||
jsonOk(null, 'Logout berhasil');
|
||||
}
|
||||
|
||||
if ($method === 'GET' && $action === 'check') {
|
||||
if (!empty($_SESSION['user'])) {
|
||||
jsonOk($_SESSION['user'], 'Session aktif');
|
||||
}
|
||||
jsonError('Tidak terautentikasi', 401);
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/bantuan.php — CRUD Histori Bantuan Sosial
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// Bantuan hanya bisa diakses oleh user yang login
|
||||
$user = requireAuth();
|
||||
|
||||
// ── GET: List bantuan untuk warga tertentu ──────────────────
|
||||
if ($method === 'GET') {
|
||||
$wargaId = getParam('warga_id');
|
||||
if (!$wargaId) jsonError('ID warga wajib disertakan.');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, tanggal, jenis, nominal, sumber_dana AS sumber, keterangan
|
||||
FROM histori_bantuan
|
||||
WHERE warga_id = ?
|
||||
ORDER BY tanggal DESC"
|
||||
);
|
||||
$stmt->execute([$wargaId]);
|
||||
jsonOk($stmt->fetchAll());
|
||||
}
|
||||
|
||||
// ── POST: Tambah bantuan baru ────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
requireEdit();
|
||||
$d = body();
|
||||
|
||||
if (empty($d['warga_id'])) jsonError('ID warga wajib diisi.');
|
||||
if (empty($d['tanggal'])) jsonError('Tanggal wajib diisi.');
|
||||
if (empty($d['jenis'])) jsonError('Program bantuan wajib dipilih.');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO histori_bantuan (warga_id, tanggal, jenis, nominal, sumber_dana, keterangan, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['warga_id'],
|
||||
$d['tanggal'],
|
||||
$d['jenis'],
|
||||
$d['nominal'] ?? 0,
|
||||
$d['sumber_dana'] ?? '',
|
||||
$d['keterangan'] ?? '',
|
||||
$user['id']
|
||||
]);
|
||||
|
||||
jsonOk(['id' => $db->lastInsertId()], 'Data bantuan berhasil ditambahkan');
|
||||
}
|
||||
|
||||
// ── DELETE: Hapus bantuan ────────────────────────────────────
|
||||
if ($method === 'DELETE') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID bantuan wajib disertakan.');
|
||||
|
||||
$db->prepare("DELETE FROM histori_bantuan WHERE id = ?")->execute([$id]);
|
||||
jsonOk(null, 'Data bantuan berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/config.php — Konfigurasi Database & Helper Functions
|
||||
// GeoSosial — Sistem Informasi Sosial Kota Pontianak
|
||||
// ============================================================
|
||||
|
||||
// Pastikan PHP errors tidak merusak output JSON
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', '0');
|
||||
ini_set('display_startup_errors', '0');
|
||||
// Mulai output buffering untuk tangkap output tak terduga
|
||||
ob_start();
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'db');
|
||||
define('DB_PORT', getenv('DB_PORT') ?: '3306');
|
||||
define('DB_NAME', getenv('DB_NAME_GEOSOSIAL') ?: 'geososial');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASS') ?: 'root');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// CORS Headers
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit(); }
|
||||
|
||||
// Session
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_name('geososial_session');
|
||||
session_start();
|
||||
}
|
||||
|
||||
// PDO Connection
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||
try {
|
||||
$pdo = new PDO($dsn, 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) {
|
||||
jsonError('Koneksi database gagal: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// Response helpers — bersihkan output buffer dulu agar tidak ada PHP warning yang merusak JSON
|
||||
function jsonOk($data = null, string $msg = 'OK'): void {
|
||||
if (ob_get_level()) ob_clean();
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'data' => $data], JSON_UNESCAPED_UNICODE);
|
||||
exit();
|
||||
}
|
||||
function jsonError(string $msg, int $code = 400): void {
|
||||
if (ob_get_level()) ob_clean();
|
||||
http_response_code($code);
|
||||
echo json_encode(['success' => false, 'message' => $msg], JSON_UNESCAPED_UNICODE);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Auth check
|
||||
function requireAuth(): array {
|
||||
if (empty($_SESSION['user'])) {
|
||||
jsonError('Tidak terautentikasi. Silakan login.', 401);
|
||||
}
|
||||
return $_SESSION['user'];
|
||||
}
|
||||
function requireEdit(): array {
|
||||
$user = requireAuth();
|
||||
if (!$user['can_edit']) jsonError('Anda tidak memiliki hak akses edit.', 403);
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Input helper
|
||||
function body(): array {
|
||||
$raw = file_get_contents('php://input');
|
||||
return json_decode($raw, true) ?? [];
|
||||
}
|
||||
function getParam(string $key, $default = null) {
|
||||
return $_GET[$key] ?? $default;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/dashboard.php — Agregasi Data & Statistik Dashboard Eksekutif
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// Dashboard: data agregat bisa diakses publik (tidak ada data pribadi warga)
|
||||
|
||||
if ($method === 'GET') {
|
||||
// ── 1. KPI STATS ──────────────────────────────────────────
|
||||
// Total KK Terdaftar (aktif)
|
||||
$kpiKK = $db->query("SELECT COUNT(*) FROM warga WHERE status = 'aktif'")->fetchColumn();
|
||||
|
||||
// Keluarga Sangat Miskin (aktif)
|
||||
$kpiSM = $db->query("SELECT COUNT(*) FROM warga WHERE status = 'aktif' AND kategori = 'sangat_miskin'")->fetchColumn();
|
||||
|
||||
// Total Penyaluran Bantuan & Nominal
|
||||
$bantuanStats = $db->query(
|
||||
"SELECT COUNT(*) AS total_penyaluran, COALESCE(SUM(nominal), 0) AS total_nominal
|
||||
FROM histori_bantuan"
|
||||
)->fetch();
|
||||
$kpiBn = $bantuanStats['total_penyaluran'];
|
||||
$kpiBnNom = $bantuanStats['total_nominal'];
|
||||
|
||||
// Penyakit Terbanyak
|
||||
$kpiPyRow = $db->query(
|
||||
"SELECT penyakit, COUNT(*) AS jumlah
|
||||
FROM riwayat_penyakit
|
||||
GROUP BY penyakit
|
||||
ORDER BY jumlah DESC
|
||||
LIMIT 1"
|
||||
)->fetch();
|
||||
$kpiPy = $kpiPyRow ? $kpiPyRow['penyakit'] : 'Tidak Ada';
|
||||
|
||||
// ── 2. CHART 1: KK Miskin per Kelurahan (Top 10) ──────────
|
||||
$chartKelRows = $db->query(
|
||||
"SELECT k.nama AS kelurahan, COUNT(w.id) AS jumlah
|
||||
FROM warga w
|
||||
LEFT JOIN kelurahan k ON w.kelurahan_id = k.id
|
||||
WHERE w.status = 'aktif'
|
||||
GROUP BY w.kelurahan_id
|
||||
ORDER BY jumlah DESC
|
||||
LIMIT 10"
|
||||
)->fetchAll();
|
||||
|
||||
$chartKel = [
|
||||
'labels' => array_column($chartKelRows, 'kelurahan'),
|
||||
'data' => array_map('intval', array_column($chartKelRows, 'jumlah'))
|
||||
];
|
||||
|
||||
// ── 3. CHART 2: Distribusi Kategori Kemiskinan ────────────
|
||||
$chartKatRows = $db->query(
|
||||
"SELECT kategori, COUNT(*) AS jumlah
|
||||
FROM warga
|
||||
WHERE status = 'aktif'
|
||||
GROUP BY kategori"
|
||||
)->fetchAll();
|
||||
|
||||
$katCounts = ['sangat_miskin' => 0, 'miskin' => 0, 'hampir_miskin' => 0];
|
||||
foreach ($chartKatRows as $row) {
|
||||
$katCounts[$row['kategori']] = (int)$row['jumlah'];
|
||||
}
|
||||
$chartKat = [
|
||||
'labels' => ['Sangat Miskin', 'Miskin', 'Hampir Miskin'],
|
||||
'data' => [$katCounts['sangat_miskin'], $katCounts['miskin'], $katCounts['hampir_miskin']]
|
||||
];
|
||||
|
||||
// ── 4. CHART 3: Penyaluran per Program Bantuan ────────────
|
||||
$chartBnRows = $db->query(
|
||||
"SELECT jenis, COUNT(*) AS jumlah
|
||||
FROM histori_bantuan
|
||||
GROUP BY jenis
|
||||
ORDER BY jumlah DESC"
|
||||
)->fetchAll();
|
||||
|
||||
$chartBn = [
|
||||
'labels' => array_column($chartBnRows, 'jenis'),
|
||||
'data' => array_map('intval', array_column($chartBnRows, 'jumlah'))
|
||||
];
|
||||
|
||||
// ── 5. CHART 4: Prevalensi Penyakit ───────────────────────
|
||||
$chartPyRows = $db->query(
|
||||
"SELECT penyakit, COUNT(*) AS jumlah
|
||||
FROM riwayat_penyakit
|
||||
GROUP BY penyakit
|
||||
ORDER BY jumlah DESC"
|
||||
)->fetchAll();
|
||||
|
||||
$chartPy = [
|
||||
'labels' => array_column($chartPyRows, 'penyakit'),
|
||||
'data' => array_map('intval', array_column($chartPyRows, 'jumlah'))
|
||||
];
|
||||
|
||||
// ── 6. TABEL RANKING KELURAHAN LENGKAP ────────────────────
|
||||
$rankingRows = $db->query(
|
||||
"SELECT
|
||||
k.nama AS kelurahan,
|
||||
k.kecamatan,
|
||||
COUNT(CASE WHEN w.kategori = 'sangat_miskin' AND w.status = 'aktif' THEN 1 END) AS sangat_miskin,
|
||||
COUNT(CASE WHEN w.kategori = 'miskin' AND w.status = 'aktif' THEN 1 END) AS miskin,
|
||||
COUNT(CASE WHEN w.kategori = 'hampir_miskin' AND w.status = 'aktif' THEN 1 END) AS hampir_miskin,
|
||||
COUNT(CASE WHEN w.status = 'aktif' THEN 1 END) AS total,
|
||||
COALESCE(SUM(hb.nominal), 0) AS total_bantuan,
|
||||
AVG(CASE WHEN w.status = 'aktif' AND w.lat IS NOT NULL AND w.lat != 0 THEN w.lat END) AS avg_lat,
|
||||
AVG(CASE WHEN w.status = 'aktif' AND w.lng IS NOT NULL AND w.lng != 0 THEN w.lng END) AS avg_lng
|
||||
FROM kelurahan k
|
||||
LEFT JOIN warga w ON w.kelurahan_id = k.id
|
||||
LEFT JOIN histori_bantuan hb ON hb.warga_id = w.id
|
||||
GROUP BY k.id
|
||||
ORDER BY total DESC, kelurahan ASC"
|
||||
)->fetchAll();
|
||||
|
||||
$ranking = [];
|
||||
foreach ($rankingRows as $r) {
|
||||
$ranking[] = [
|
||||
'kelurahan' => $r['kelurahan'],
|
||||
'kecamatan' => $r['kecamatan'],
|
||||
'sangat_miskin' => (int)$r['sangat_miskin'],
|
||||
'miskin' => (int)$r['miskin'],
|
||||
'hampir_miskin' => (int)$r['hampir_miskin'],
|
||||
'total' => (int)$r['total'],
|
||||
'total_bantuan' => (float)$r['total_bantuan'],
|
||||
'lat' => $r['avg_lat'] ? (float)$r['avg_lat'] : null,
|
||||
'lng' => $r['avg_lng'] ? (float)$r['avg_lng'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
// Gabungkan hasil statistik
|
||||
$dashboardData = [
|
||||
'kpis' => [
|
||||
'total_kk' => $kpiKK,
|
||||
'sangat_miskin' => $kpiSM,
|
||||
'total_penyaluran' => $kpiBn,
|
||||
'total_nominal' => $kpiBnNom,
|
||||
'penyakit_top' => $kpiPy
|
||||
],
|
||||
'charts' => [
|
||||
'kelurahan' => $chartKel,
|
||||
'kategori' => $chartKat,
|
||||
'bantuan' => $chartBn,
|
||||
'penyakit' => $chartPy
|
||||
],
|
||||
'ranking' => $ranking
|
||||
];
|
||||
|
||||
jsonOk($dashboardData);
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/fasilitas_umum.php — GET Fasilitas Umum
|
||||
// GeoSosial — Sistem Informasi Sosial Kota Pontianak
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$kategori = getParam('kategori');
|
||||
if ($kategori) {
|
||||
$stmt = $db->prepare("SELECT * FROM fasilitas_umum WHERE kategori = ? ORDER BY nama");
|
||||
$stmt->execute([$kategori]);
|
||||
$rows = $stmt->fetchAll();
|
||||
} else {
|
||||
$rows = $db->query("SELECT * FROM fasilitas_umum ORDER BY kategori, nama")->fetchAll();
|
||||
}
|
||||
jsonOk($rows);
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/kelurahan.php — Master data kelurahan
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$db = getDB();
|
||||
$rows = $db->query("SELECT id, nama, kecamatan FROM kelurahan ORDER BY kecamatan, nama")->fetchAll();
|
||||
jsonOk($rows);
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/laporan.php — CRUD & Approval Laporan Masyarakat
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// ── POST: Submit laporan publik baru (Tanpa Login) ──────────
|
||||
if ($method === 'POST') {
|
||||
$d = body();
|
||||
|
||||
if (empty($d['nama'])) jsonError('Nama warga yang dilaporkan wajib diisi.');
|
||||
if (empty($d['kelurahan_id'])) jsonError('Kelurahan wajib dipilih.');
|
||||
if (empty($d['keterangan'])) jsonError('Keterangan kondisi wajib diisi.');
|
||||
|
||||
// Generate No. Laporan: RPT-YYYY-XXXX (cth: RPT-2026-001)
|
||||
$year = date('Y');
|
||||
|
||||
// Lock table / select max count for transaction safety
|
||||
$stmtCount = $db->prepare("SELECT COUNT(*) FROM laporan WHERE YEAR(created_at) = ?");
|
||||
$stmtCount->execute([$year]);
|
||||
$count = $stmtCount->fetchColumn();
|
||||
$nextNo = $count + 1;
|
||||
$noLaporan = "RPT-" . $year . "-" . str_pad($nextNo, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
// Pastikan no_laporan benar-benar unik
|
||||
$stmtCheck = $db->prepare("SELECT COUNT(*) FROM laporan WHERE no_laporan = ?");
|
||||
$stmtCheck->execute([$noLaporan]);
|
||||
if ($stmtCheck->fetchColumn() > 0) {
|
||||
$noLaporan = "RPT-" . $year . "-" . str_pad($nextNo + 1, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO laporan (no_laporan, nama_pelapor, hp_pelapor, nama_dilaporkan, nik,
|
||||
kelurahan_id, kategori, jumlah_anggota, penghasilan, keterangan,
|
||||
lat, lng, status, tgl_laporan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'menunggu', CURRENT_DATE())"
|
||||
);
|
||||
$stmt->execute([
|
||||
$noLaporan,
|
||||
$d['pelapor'] ?? null,
|
||||
$d['hp'] ?? null,
|
||||
$d['nama'],
|
||||
$d['nik'] ?? null,
|
||||
$d['kelurahan_id'],
|
||||
$d['kategori'] ?? 'miskin',
|
||||
$d['jumlah_anggota'] ?? 1,
|
||||
$d['penghasilan'] ?? 0,
|
||||
$d['keterangan'],
|
||||
$d['lat'] ?? null,
|
||||
$d['lng'] ?? null
|
||||
]);
|
||||
|
||||
$lapId = $db->lastInsertId();
|
||||
jsonOk(['id' => $lapId, 'no' => $noLaporan], 'Laporan berhasil dikirim');
|
||||
}
|
||||
|
||||
// Laporan read/update butuh autentikasi
|
||||
$user = requireAuth();
|
||||
|
||||
// ── GET: List semua laporan ──────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$rows = $db->query(
|
||||
"SELECT l.id, l.no_laporan AS no, l.nama_pelapor AS pelapor, l.hp_pelapor AS hp,
|
||||
l.nama_dilaporkan AS nama, l.nik, l.kategori, l.jumlah_anggota AS jml,
|
||||
l.penghasilan AS ph, l.keterangan AS ket, l.lat, l.lng, l.status,
|
||||
l.tgl_laporan AS tgl, l.created_at, k.id AS kelurahan_id, k.nama AS kel, k.kecamatan
|
||||
FROM laporan l
|
||||
LEFT JOIN kelurahan k ON l.kelurahan_id = k.id
|
||||
ORDER BY l.created_at DESC"
|
||||
)->fetchAll();
|
||||
|
||||
jsonOk($rows);
|
||||
}
|
||||
|
||||
// ── PUT: Approve atau Reject laporan ──────────────────────────
|
||||
if ($method === 'PUT') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
$action = getParam('action'); // 'approve' atau 'reject'
|
||||
|
||||
if (!$id) jsonError('ID laporan wajib disertakan.');
|
||||
if (!in_array($action, ['approve', 'reject'])) jsonError('Aksi tidak valid.');
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
// Ambil data laporan
|
||||
$stmtLap = $db->prepare("SELECT * FROM laporan WHERE id = ?");
|
||||
$stmtLap->execute([$id]);
|
||||
$lap = $stmtLap->fetch();
|
||||
|
||||
if (!$lap) jsonError('Laporan tidak ditemukan.');
|
||||
if ($lap['status'] !== 'menunggu') jsonError('Laporan sudah diproses sebelumnya.');
|
||||
|
||||
if ($action === 'reject') {
|
||||
$stmtUp = $db->prepare("UPDATE laporan SET status = 'ditolak', tgl_proses = CURRENT_TIMESTAMP() WHERE id = ?");
|
||||
$stmtUp->execute([$id]);
|
||||
$db->commit();
|
||||
jsonOk(null, 'Laporan ditolak');
|
||||
}
|
||||
|
||||
if ($action === 'approve') {
|
||||
// 1. Tambahkan ke tabel warga
|
||||
$stmtWarga = $db->prepare(
|
||||
"INSERT INTO warga (nama, nik, kelurahan_id, lat, lng, kategori, jumlah_anggota,
|
||||
penghasilan, tgl_lahir, pendidikan, keterangan, status, pelapor, no_laporan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, '', ?, 'aktif', ?, ?)"
|
||||
);
|
||||
$stmtWarga->execute([
|
||||
$lap['nama_dilaporkan'],
|
||||
$lap['nik'],
|
||||
$lap['kelurahan_id'],
|
||||
$lap['lat'] ?? -0.0230,
|
||||
$lap['lng'] ?? 109.3420,
|
||||
$lap['kategori'],
|
||||
$lap['jumlah_anggota'] ?? 1,
|
||||
$lap['penghasilan'] ?? 0,
|
||||
$lap['keterangan'] . ' (Laporan: ' . $lap['no_laporan'] . ')',
|
||||
$lap['nama_pelapor'],
|
||||
$lap['no_laporan']
|
||||
]);
|
||||
$wargaId = $db->lastInsertId();
|
||||
|
||||
// 2. Tambahkan KK sebagai anggota keluarga pertama
|
||||
$stmtAnggota = $db->prepare(
|
||||
"INSERT INTO anggota_keluarga (warga_id, nama, usia, hubungan)
|
||||
VALUES (?, ?, 0, 'Kepala Keluarga')"
|
||||
);
|
||||
$stmtAnggota->execute([$wargaId, $lap['nama_dilaporkan']]);
|
||||
|
||||
// 3. Update status laporan menjadi disetujui & tautkan warga_id
|
||||
$stmtUp = $db->prepare(
|
||||
"UPDATE laporan
|
||||
SET status = 'disetujui', warga_id = ?, approved_by = ?, tgl_proses = CURRENT_TIMESTAMP()
|
||||
WHERE id = ?"
|
||||
);
|
||||
$stmtUp->execute([$wargaId, $user['id'], $id]);
|
||||
|
||||
$db->commit();
|
||||
jsonOk(['warga_id' => $wargaId], 'Laporan disetujui dan warga berhasil ditambahkan');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
jsonError('Gagal memproses laporan: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// api/rumah_ibadah.php — CRUD Rumah Ibadah
|
||||
require_once __DIR__ . '/config.php';
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
if ($method === 'GET') {
|
||||
$rows = $db->query(
|
||||
"SELECT ri.*, k.nama AS kelurahan, k.kecamatan
|
||||
FROM rumah_ibadah ri
|
||||
LEFT JOIN kelurahan k ON ri.kelurahan_id = k.id
|
||||
ORDER BY ri.tipe, ri.nama"
|
||||
)->fetchAll();
|
||||
jsonOk($rows);
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
requireEdit();
|
||||
$d = body();
|
||||
if (empty($d['nama'])) jsonError('Nama wajib diisi.');
|
||||
if (!isset($d['lat'], $d['lng'])) jsonError('Koordinat wajib diisi.');
|
||||
$ikonMap = ['masjid'=>'🕌','gereja'=>'⛪','pura'=>'🛕','vihara'=>'🏛️','klenteng'=>'🏯'];
|
||||
$tipe = $d['tipe'] ?? 'masjid';
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO rumah_ibadah (nama, tipe, ikon, lat, lng, kapasitas, kelurahan_id, alamat)
|
||||
VALUES (?,?,?,?,?,?,?,?)"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['nama'], $tipe, $ikonMap[$tipe] ?? '🛕',
|
||||
$d['lat'], $d['lng'],
|
||||
$d['kapasitas'] ?? 0,
|
||||
!empty($d['kelurahan_id']) ? $d['kelurahan_id'] : null,
|
||||
$d['alamat'] ?? ''
|
||||
]);
|
||||
jsonOk(['id' => $db->lastInsertId()], 'Rumah ibadah berhasil ditambahkan');
|
||||
}
|
||||
|
||||
if ($method === 'PUT') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
$d = body();
|
||||
if (empty($d['nama'])) jsonError('Nama wajib diisi.');
|
||||
if (!isset($d['lat'], $d['lng'])) jsonError('Koordinat wajib diisi.');
|
||||
$ikonMap = ['masjid'=>'🕌','gereja'=>'⛪','pura'=>'🛕','vihara'=>'🏛️','klenteng'=>'🏯'];
|
||||
$tipe = $d['tipe'] ?? 'masjid';
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE rumah_ibadah
|
||||
SET nama=?, tipe=?, ikon=?, lat=?, lng=?, kapasitas=?, kelurahan_id=?, alamat=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['nama'], $tipe, $ikonMap[$tipe] ?? '🛕',
|
||||
$d['lat'], $d['lng'],
|
||||
$d['kapasitas'] ?? 0,
|
||||
!empty($d['kelurahan_id']) ? $d['kelurahan_id'] : null,
|
||||
$d['alamat'] ?? '',
|
||||
$id
|
||||
]);
|
||||
jsonOk(null, 'Rumah ibadah berhasil diperbarui');
|
||||
}
|
||||
|
||||
if ($method === 'DELETE') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
$db->prepare("DELETE FROM rumah_ibadah WHERE id=?")->execute([$id]);
|
||||
jsonOk(null, 'Rumah ibadah berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/users.php — Manajemen Pengguna (Admin only)
|
||||
// GET — list semua pengguna
|
||||
// PUT ?id= — update jabatan, role, can_edit, aktif
|
||||
// POST — tambah pengguna baru
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// ── GET: list semua pengguna ────────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
requireAuth();
|
||||
$rows = $db->query(
|
||||
"SELECT id, username, role, jabatan, can_edit, is_admin, is_wali, aktif, created_at
|
||||
FROM users ORDER BY id"
|
||||
)->fetchAll();
|
||||
// Hapus password dari hasil
|
||||
jsonOk($rows);
|
||||
}
|
||||
|
||||
// ── PUT: update data pengguna ───────────────────────────────
|
||||
if ($method === 'PUT') {
|
||||
$user = requireAuth();
|
||||
if (!$user['is_admin']) jsonError('Hanya admin yang dapat mengubah data pengguna.', 403);
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
$d = body();
|
||||
|
||||
$fields = [];
|
||||
$params = [];
|
||||
|
||||
if (isset($d['jabatan'])) { $fields[] = 'jabatan=?'; $params[] = $d['jabatan']; }
|
||||
if (isset($d['role'])) { $fields[] = 'role=?'; $params[] = $d['role'];
|
||||
$params_extra = [];
|
||||
$isAdmin = $d['role'] === 'admin' ? 1 : 0;
|
||||
$isWali = $d['role'] === 'walikota' ? 1 : 0;
|
||||
$fields[] = 'is_admin=?'; $params[] = $isAdmin;
|
||||
$fields[] = 'is_wali=?'; $params[] = $isWali; }
|
||||
if (isset($d['can_edit'])) { $fields[] = 'can_edit=?'; $params[] = $d['can_edit'] ? 1 : 0; }
|
||||
if (isset($d['aktif'])) { $fields[] = 'aktif=?'; $params[] = $d['aktif'] ? 1 : 0; }
|
||||
if (isset($d['password']) && strlen($d['password']) >= 6) {
|
||||
$fields[] = 'password=?';
|
||||
$params[] = password_hash($d['password'], PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
if (empty($fields)) jsonError('Tidak ada data yang diubah.');
|
||||
|
||||
$params[] = $id;
|
||||
$db->prepare("UPDATE users SET " . implode(', ', $fields) . " WHERE id=?")->execute($params);
|
||||
jsonOk(null, 'Data pengguna berhasil diperbarui');
|
||||
}
|
||||
|
||||
// ── POST: tambah pengguna baru ──────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
$user = requireAuth();
|
||||
if (!$user['is_admin']) jsonError('Hanya admin yang dapat menambah pengguna.', 403);
|
||||
$d = body();
|
||||
if (empty($d['username'])) jsonError('Username wajib diisi.');
|
||||
if (empty($d['password']) || strlen($d['password']) < 6) jsonError('Password minimal 6 karakter.');
|
||||
|
||||
// Cek username sudah ada
|
||||
$chk = $db->prepare("SELECT id FROM users WHERE username=?");
|
||||
$chk->execute([$d['username']]);
|
||||
if ($chk->fetch()) jsonError('Username sudah digunakan.');
|
||||
|
||||
$role = $d['role'] ?? 'viewer';
|
||||
$isAdmin = $role === 'admin' ? 1 : 0;
|
||||
$isWali = $role === 'walikota' ? 1 : 0;
|
||||
$canEdit = in_array($role, ['admin', 'petugas']) ? 1 : 0;
|
||||
if (isset($d['can_edit'])) $canEdit = $d['can_edit'] ? 1 : 0;
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO users (username, password, role, jabatan, can_edit, is_admin, is_wali, aktif)
|
||||
VALUES (?,?,?,?,?,?,?,1)"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['username'],
|
||||
password_hash($d['password'], PASSWORD_BCRYPT),
|
||||
$role,
|
||||
$d['jabatan'] ?? '',
|
||||
$canEdit,
|
||||
$isAdmin,
|
||||
$isWali,
|
||||
]);
|
||||
jsonOk(['id' => $db->lastInsertId()], 'Pengguna berhasil ditambahkan');
|
||||
}
|
||||
|
||||
// ── DELETE: nonaktifkan pengguna ────────────────────────────
|
||||
if ($method === 'DELETE') {
|
||||
$user = requireAuth();
|
||||
if (!$user['is_admin']) jsonError('Hanya admin yang dapat menghapus pengguna.', 403);
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
// Jangan hapus diri sendiri
|
||||
if ($id == $user['id']) jsonError('Tidak dapat menghapus akun sendiri.');
|
||||
$db->prepare("DELETE FROM users WHERE id=?")->execute([$id]);
|
||||
jsonOk(null, 'Pengguna berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/warga.php — CRUD Data Warga (Kepala Keluarga)
|
||||
// GET ?kat=&kel_id=&kec=&status=aktif — list warga
|
||||
// POST — tambah warga
|
||||
// PUT ?id= — update warga
|
||||
// DELETE ?id= — hapus warga
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = getDB();
|
||||
|
||||
// ── GET: list warga dengan filter ──────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$where = ["w.status = 'aktif'"];
|
||||
$params = [];
|
||||
|
||||
if ($kat = getParam('kat')) { $where[] = "w.kategori = ?"; $params[] = $kat; }
|
||||
if ($kid = getParam('kel_id')){ $where[] = "w.kelurahan_id = ?"; $params[] = $kid; }
|
||||
if ($kec = getParam('kec')) { $where[] = "k.kecamatan = ?"; $params[] = $kec; }
|
||||
|
||||
$sql = "SELECT w.id, w.nama, w.nik, w.lat, w.lng, w.kategori,
|
||||
w.jumlah_anggota, w.penghasilan, w.tgl_lahir, w.pendidikan,
|
||||
w.keterangan, w.status, w.pelapor, w.no_laporan, w.created_at,
|
||||
k.id AS kelurahan_id, k.nama AS kelurahan, k.kecamatan
|
||||
FROM warga w
|
||||
LEFT JOIN kelurahan k ON w.kelurahan_id = k.id
|
||||
WHERE " . implode(' AND ', $where) . "
|
||||
ORDER BY w.created_at DESC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$wargaList = $stmt->fetchAll();
|
||||
|
||||
// Ambil anggota & penyakit untuk setiap warga
|
||||
foreach ($wargaList as &$w) {
|
||||
$stmt2 = $db->prepare(
|
||||
"SELECT a.id, a.nama, a.usia, a.hubungan,
|
||||
GROUP_CONCAT(rp.penyakit SEPARATOR ',') AS penyakit_csv
|
||||
FROM anggota_keluarga a
|
||||
LEFT JOIN riwayat_penyakit rp ON rp.anggota_id = a.id
|
||||
WHERE a.warga_id = ?
|
||||
GROUP BY a.id"
|
||||
);
|
||||
$stmt2->execute([$w['id']]);
|
||||
$anggota = $stmt2->fetchAll();
|
||||
foreach ($anggota as &$a) {
|
||||
$a['penyakit'] = $a['penyakit_csv'] ? explode(',', $a['penyakit_csv']) : [];
|
||||
unset($a['penyakit_csv']);
|
||||
}
|
||||
$w['anggota'] = $anggota;
|
||||
|
||||
// Riwayat penyakit KK (dari anggota dengan nama sama / index 0)
|
||||
$rp_kk = [];
|
||||
if (!empty($anggota)) {
|
||||
$rp_kk = $anggota[0]['penyakit'];
|
||||
}
|
||||
$w['riwayat_penyakit'] = $rp_kk;
|
||||
|
||||
// Histori bantuan
|
||||
$stmt3 = $db->prepare(
|
||||
"SELECT id, tanggal, jenis, nominal, sumber_dana AS sumber, keterangan
|
||||
FROM histori_bantuan WHERE warga_id = ? ORDER BY tanggal DESC"
|
||||
);
|
||||
$stmt3->execute([$w['id']]);
|
||||
$w['histori_bantuan'] = $stmt3->fetchAll();
|
||||
}
|
||||
|
||||
jsonOk($wargaList);
|
||||
}
|
||||
|
||||
// ── POST: tambah warga ──────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
requireEdit();
|
||||
$d = body();
|
||||
|
||||
if (empty($d['nama'])) jsonError('Nama wajib diisi.');
|
||||
if (!isset($d['lat'], $d['lng'])) jsonError('Koordinat lokasi wajib diisi.');
|
||||
if (empty($d['kelurahan_id'])) jsonError('Kelurahan wajib dipilih.');
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO warga (nama, nik, kelurahan_id, lat, lng, kategori, jumlah_anggota,
|
||||
penghasilan, tgl_lahir, pendidikan, keterangan, pelapor, no_laporan)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['nama'],
|
||||
$d['nik'] ?? null,
|
||||
$d['kelurahan_id'],
|
||||
$d['lat'],
|
||||
$d['lng'],
|
||||
$d['kategori'] ?? 'miskin',
|
||||
$d['jumlah_anggota'] ?? 1,
|
||||
$d['penghasilan'] ?? 0,
|
||||
!empty($d['tgl_lahir']) ? $d['tgl_lahir'] : null,
|
||||
$d['pendidikan'] ?? '',
|
||||
$d['keterangan'] ?? '',
|
||||
$d['pelapor'] ?? null,
|
||||
$d['no_laporan'] ?? null,
|
||||
]);
|
||||
$wargaId = $db->lastInsertId();
|
||||
|
||||
// 1. Simpan Kepala Keluarga sebagai anggota keluarga pertama
|
||||
$usiaKk = 0;
|
||||
if (!empty($d['tgl_lahir'])) {
|
||||
try {
|
||||
$birthDate = new DateTime($d['tgl_lahir']);
|
||||
$today = new DateTime();
|
||||
$usiaKk = $today->diff($birthDate)->y;
|
||||
} catch (Exception $e) {
|
||||
$usiaKk = 0;
|
||||
}
|
||||
}
|
||||
$stmtKk = $db->prepare(
|
||||
"INSERT INTO anggota_keluarga (warga_id, nama, usia, hubungan) VALUES (?, ?, ?, 'Kepala Keluarga')"
|
||||
);
|
||||
$stmtKk->execute([$wargaId, $d['nama'], $usiaKk]);
|
||||
$kkAnggotaId = $db->lastInsertId();
|
||||
|
||||
$kkPenyakit = $d['rp'] ?? $d['riwayat_penyakit'] ?? [];
|
||||
foreach ($kkPenyakit as $py) {
|
||||
if (!$py) continue;
|
||||
$db->prepare("INSERT INTO riwayat_penyakit (anggota_id, penyakit) VALUES (?, ?)")
|
||||
->execute([$kkAnggotaId, $py]);
|
||||
}
|
||||
|
||||
// 2. Simpan anggota keluarga lainnya + penyakit mereka
|
||||
foreach (($d['anggota'] ?? []) as $a) {
|
||||
if (empty($a['nama'])) continue;
|
||||
|
||||
// Hindari duplikasi jika KK dikirim lagi di list anggota
|
||||
if (strtolower(trim($a['nama'])) === strtolower(trim($d['nama'])) && ($a['hub'] ?? $a['hubungan'] ?? '') === 'Kepala Keluarga') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmtA = $db->prepare(
|
||||
"INSERT INTO anggota_keluarga (warga_id, nama, usia, hubungan) VALUES (?,?,?,?)"
|
||||
);
|
||||
$stmtA->execute([$wargaId, $a['nama'], $a['usia'] ?? 0, $a['hub'] ?? $a['hubungan'] ?? 'Anak']);
|
||||
$anggotaId = $db->lastInsertId();
|
||||
|
||||
foreach (($a['penyakit'] ?? $a['py'] ?? []) as $py) {
|
||||
if (!$py) continue;
|
||||
$db->prepare("INSERT INTO riwayat_penyakit (anggota_id, penyakit) VALUES (?,?)")
|
||||
->execute([$anggotaId, $py]);
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
jsonOk(['id' => $wargaId], 'Data warga berhasil ditambahkan');
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
jsonError('Gagal menyimpan: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ── PUT: update data warga ─────────────────────────────────
|
||||
if ($method === 'PUT') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
$d = body();
|
||||
if (empty($d['nama'])) jsonError('Nama wajib diisi.');
|
||||
if (!isset($d['lat'], $d['lng'])) jsonError('Koordinat lokasi wajib diisi.');
|
||||
if (empty($d['kelurahan_id'])) jsonError('Kelurahan wajib dipilih.');
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
// 1. Update data kepala keluarga
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE warga SET nama=?, nik=?, kelurahan_id=?, lat=?, lng=?,
|
||||
kategori=?, jumlah_anggota=?, penghasilan=?,
|
||||
tgl_lahir=?, pendidikan=?, keterangan=?
|
||||
WHERE id=?"
|
||||
);
|
||||
$stmt->execute([
|
||||
$d['nama'],
|
||||
$d['nik'] ?? null,
|
||||
$d['kelurahan_id'],
|
||||
$d['lat'],
|
||||
$d['lng'],
|
||||
$d['kategori'] ?? 'miskin',
|
||||
$d['jumlah_anggota'] ?? 1,
|
||||
$d['penghasilan'] ?? 0,
|
||||
!empty($d['tgl_lahir']) ? $d['tgl_lahir'] : null,
|
||||
$d['pendidikan'] ?? '',
|
||||
$d['keterangan'] ?? '',
|
||||
$id
|
||||
]);
|
||||
|
||||
// 2. Hapus anggota lama beserta riwayat penyakitnya
|
||||
$oldAnggota = $db->prepare("SELECT id FROM anggota_keluarga WHERE warga_id=?");
|
||||
$oldAnggota->execute([$id]);
|
||||
foreach ($oldAnggota->fetchAll() as $oa) {
|
||||
$db->prepare("DELETE FROM riwayat_penyakit WHERE anggota_id=?")->execute([$oa['id']]);
|
||||
}
|
||||
$db->prepare("DELETE FROM anggota_keluarga WHERE warga_id=?")->execute([$id]);
|
||||
|
||||
// 3. Simpan KK sebagai anggota keluarga pertama
|
||||
$usiaKk = 0;
|
||||
if (!empty($d['tgl_lahir'])) {
|
||||
try {
|
||||
$birthDate = new DateTime($d['tgl_lahir']);
|
||||
$today = new DateTime();
|
||||
$usiaKk = $today->diff($birthDate)->y;
|
||||
} catch (Exception $e) { $usiaKk = 0; }
|
||||
}
|
||||
$stmtKk = $db->prepare(
|
||||
"INSERT INTO anggota_keluarga (warga_id, nama, usia, hubungan) VALUES (?, ?, ?, 'Kepala Keluarga')"
|
||||
);
|
||||
$stmtKk->execute([$id, $d['nama'], $usiaKk]);
|
||||
$kkAnggotaId = $db->lastInsertId();
|
||||
|
||||
$kkPenyakit = $d['rp'] ?? $d['riwayat_penyakit'] ?? [];
|
||||
foreach ($kkPenyakit as $py) {
|
||||
if (!$py) continue;
|
||||
$db->prepare("INSERT INTO riwayat_penyakit (anggota_id, penyakit) VALUES (?, ?)")
|
||||
->execute([$kkAnggotaId, $py]);
|
||||
}
|
||||
|
||||
// 4. Simpan anggota keluarga lainnya
|
||||
foreach (($d['anggota'] ?? []) as $a) {
|
||||
if (empty($a['nama'])) continue;
|
||||
if (strtolower(trim($a['nama'])) === strtolower(trim($d['nama'])) &&
|
||||
($a['hub'] ?? $a['hubungan'] ?? '') === 'Kepala Keluarga') {
|
||||
continue;
|
||||
}
|
||||
$stmtA = $db->prepare(
|
||||
"INSERT INTO anggota_keluarga (warga_id, nama, usia, hubungan) VALUES (?,?,?,?)"
|
||||
);
|
||||
$stmtA->execute([$id, $a['nama'], $a['usia'] ?? 0, $a['hub'] ?? $a['hubungan'] ?? 'Anak']);
|
||||
$anggotaId = $db->lastInsertId();
|
||||
foreach (($a['penyakit'] ?? $a['py'] ?? []) as $py) {
|
||||
if (!$py) continue;
|
||||
$db->prepare("INSERT INTO riwayat_penyakit (anggota_id, penyakit) VALUES (?,?)")
|
||||
->execute([$anggotaId, $py]);
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
jsonOk(null, 'Data warga berhasil diperbarui');
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
jsonError('Gagal memperbarui: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ── DELETE: hapus warga ────────────────────────────────────
|
||||
if ($method === 'DELETE') {
|
||||
requireEdit();
|
||||
$id = getParam('id');
|
||||
if (!$id) jsonError('ID wajib disertakan.');
|
||||
$db->prepare("UPDATE warga SET status='nonaktif' WHERE id=?")->execute([$id]);
|
||||
jsonOk(null, 'Data warga berhasil dihapus');
|
||||
}
|
||||
|
||||
jsonError('Method tidak valid', 405);
|
||||
@@ -0,0 +1,264 @@
|
||||
-- ============================================================
|
||||
-- GeoSosial Database Schema
|
||||
-- Sistem Informasi Sosial Kota Pontianak
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS geososial CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE geososial;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: users (autentikasi)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','petugas','walikota','viewer') NOT NULL DEFAULT 'viewer',
|
||||
jabatan VARCHAR(100),
|
||||
can_edit TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_wali TINYINT(1) NOT NULL DEFAULT 0,
|
||||
aktif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: kelurahan (master data)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS kelurahan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
kecamatan VARCHAR(100) NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: rumah_ibadah
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
tipe ENUM('masjid','gereja','pura','vihara','klenteng') NOT NULL,
|
||||
ikon VARCHAR(10) DEFAULT '🕌',
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
kapasitas INT DEFAULT 0,
|
||||
kelurahan_id INT,
|
||||
alamat TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kelurahan_id) REFERENCES kelurahan(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: warga (data kepala keluarga)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS warga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
nik VARCHAR(16),
|
||||
kelurahan_id INT,
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
kategori ENUM('sangat_miskin','miskin','hampir_miskin') NOT NULL,
|
||||
jumlah_anggota INT DEFAULT 1,
|
||||
penghasilan BIGINT DEFAULT 0,
|
||||
tgl_lahir DATE,
|
||||
pendidikan ENUM('tidak_sekolah','sd','smp','sma','d3','s1','s2','') DEFAULT '',
|
||||
keterangan TEXT,
|
||||
status ENUM('aktif','nonaktif') DEFAULT 'aktif',
|
||||
pelapor VARCHAR(150),
|
||||
no_laporan VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kelurahan_id) REFERENCES kelurahan(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: anggota_keluarga
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS anggota_keluarga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
warga_id INT NOT NULL,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
usia INT DEFAULT 0,
|
||||
hubungan VARCHAR(50) DEFAULT 'Anak',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (warga_id) REFERENCES warga(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: riwayat_penyakit (per anggota keluarga)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS riwayat_penyakit (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
anggota_id INT NOT NULL,
|
||||
penyakit ENUM('hipertensi','diabetes','tbc','asma','stroke','jantung','odgj','stunting','kanker') NOT NULL,
|
||||
FOREIGN KEY (anggota_id) REFERENCES anggota_keluarga(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: histori_bantuan
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS histori_bantuan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
warga_id INT NOT NULL,
|
||||
tanggal DATE NOT NULL,
|
||||
jenis VARCHAR(50) NOT NULL,
|
||||
nominal BIGINT DEFAULT 0,
|
||||
sumber_dana VARCHAR(100),
|
||||
keterangan TEXT,
|
||||
created_by INT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (warga_id) REFERENCES warga(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: laporan (dari masyarakat)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS laporan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
no_laporan VARCHAR(20) NOT NULL UNIQUE,
|
||||
nama_pelapor VARCHAR(100),
|
||||
hp_pelapor VARCHAR(20),
|
||||
nama_dilaporkan VARCHAR(150) NOT NULL,
|
||||
nik VARCHAR(16),
|
||||
kelurahan_id INT,
|
||||
kategori ENUM('sangat_miskin','miskin','hampir_miskin') NOT NULL,
|
||||
jumlah_anggota INT DEFAULT 0,
|
||||
penghasilan BIGINT DEFAULT 0,
|
||||
keterangan TEXT NOT NULL,
|
||||
lat DECIMAL(10,7),
|
||||
lng DECIMAL(10,7),
|
||||
status ENUM('menunggu','disetujui','ditolak') DEFAULT 'menunggu',
|
||||
warga_id INT COMMENT 'warga_id jika laporan disetujui',
|
||||
approved_by INT,
|
||||
tgl_laporan DATE NOT NULL,
|
||||
tgl_proses TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kelurahan_id) REFERENCES kelurahan(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (warga_id) REFERENCES warga(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: users
|
||||
-- ============================================================
|
||||
INSERT INTO users (username, password, role, jabatan, can_edit, is_admin, is_wali) VALUES
|
||||
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 'Kadis Sosial', 1, 1, 0),
|
||||
('petugas', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'petugas', 'Staf Verifikasi', 1, 0, 0),
|
||||
('walikota', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'walikota', 'Walikota Pontianak', 0, 0, 1),
|
||||
('viewer', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'viewer', 'Pemangku Kepentingan', 0, 0, 0);
|
||||
|
||||
-- NOTE: Password hash di atas adalah hash bcrypt dari kata "password"
|
||||
-- Ubah password melalui phpMyAdmin atau script update_password.php
|
||||
-- Password asli: admin=admin123, petugas=petugas123, walikota=wali2024, viewer=viewer123
|
||||
-- (Hash akan diupdate otomatis saat pertama login via generate_passwords.php)
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: kelurahan (6 kecamatan, 24 kelurahan)
|
||||
-- ============================================================
|
||||
INSERT INTO kelurahan (nama, kecamatan) VALUES
|
||||
-- Pontianak Kota
|
||||
('Kel. Tengah', 'Pontianak Kota'),
|
||||
('Kel. Darat Sekip', 'Pontianak Kota'),
|
||||
('Kel. Sungai Bangkong', 'Pontianak Kota'),
|
||||
('Kel. Mariana', 'Pontianak Kota'),
|
||||
('Kel. Sungai Jawi', 'Pontianak Kota'),
|
||||
-- Pontianak Selatan
|
||||
('Kel. Parit Tokaya', 'Pontianak Selatan'),
|
||||
('Kel. Benua Melayu Darat', 'Pontianak Selatan'),
|
||||
('Kel. Benua Melayu Laut', 'Pontianak Selatan'),
|
||||
('Kel. Kota Baru', 'Pontianak Selatan'),
|
||||
-- Pontianak Barat
|
||||
('Kel. Sungai Beliung', 'Pontianak Barat'),
|
||||
('Kel. Pal Lima', 'Pontianak Barat'),
|
||||
('Kel. Akcaya', 'Pontianak Barat'),
|
||||
('Kel. Sungai Jawi Luar', 'Pontianak Barat'),
|
||||
-- Pontianak Utara
|
||||
('Kel. Siantan Hulu', 'Pontianak Utara'),
|
||||
('Kel. Batu Layang', 'Pontianak Utara'),
|
||||
('Kel. Saigon', 'Pontianak Utara'),
|
||||
('Kel. Siantan Hilir', 'Pontianak Utara'),
|
||||
-- Pontianak Timur
|
||||
('Kel. Tanjung Hulu', 'Pontianak Timur'),
|
||||
('Kel. Banjar Serasan', 'Pontianak Timur'),
|
||||
('Kel. Dalam Bugis', 'Pontianak Timur'),
|
||||
-- Pontianak Tenggara
|
||||
('Kel. Bangka Belitung Darat', 'Pontianak Tenggara'),
|
||||
('Kel. Bangka Belitung Laut', 'Pontianak Tenggara'),
|
||||
('Kel. Bansir Laut', 'Pontianak Tenggara'),
|
||||
('Kel. Bansir Darat', 'Pontianak Tenggara');
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: rumah_ibadah
|
||||
-- ============================================================
|
||||
INSERT INTO rumah_ibadah (nama, tipe, ikon, lat, lng, kapasitas, kelurahan_id, alamat) VALUES
|
||||
('Masjid Al-Ikhlas', 'masjid', '🕌', -0.0180000, 109.3320000, 150, 4, 'Jl. Mariana, Pontianak Kota'),
|
||||
('Masjid Raya Mujahidin', 'masjid', '🕌', -0.0428000, 109.3373000, 500, 12, 'Jl. Jend. Ahmad Yani, Pontianak Selatan'),
|
||||
('Masjid Jami Sultan', 'masjid', '🕌', -0.0400000, 109.3440000, 300, 20, 'Jl. Tanjungpura, Pontianak Timur'),
|
||||
('Masjid Nurul Hidayah', 'masjid', '🕌', -0.0100000, 109.3360000, 100, 14, 'Jl. Siantan Hulu, Pontianak Utara'),
|
||||
('Gereja GPIB Immanuel', 'gereja', '⛪', -0.0130000, 109.3480000, 200, 14, 'Jl. Sisingamangaraja, Pontianak Utara'),
|
||||
('Gereja Bethel Indonesia','gereja', '⛪', -0.0300000, 109.3370000, 180, 6, 'Jl. Parit Tokaya, Pontianak Selatan'),
|
||||
('Gereja Katedral', 'gereja', '⛪', -0.0350000, 109.3620000, 350, 15, 'Jl. Diponegoro, Pontianak Utara'),
|
||||
('Pura Girinatha', 'pura', '🛕', -0.0220000, 109.3550000, 80, 3, 'Jl. Sungai Bangkong, Pontianak Kota'),
|
||||
('Vihara Bodhisatva', 'vihara', '🏛️', -0.0080000, 109.3400000, 60, 14, 'Jl. Siantan, Pontianak Utara'),
|
||||
('Klenteng Tua Pek Kong', 'klenteng', '🏯', -0.0260000, 109.3420000, 70, 1, 'Jl. Diponegoro, Pontianak Kota');
|
||||
|
||||
-- ============================================================
|
||||
-- VIEWS berguna
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE VIEW v_warga_lengkap AS
|
||||
SELECT
|
||||
w.id, w.nama, w.nik, w.lat, w.lng, w.kategori,
|
||||
w.jumlah_anggota, w.penghasilan, w.tgl_lahir, w.pendidikan,
|
||||
w.keterangan, w.status, w.pelapor, w.no_laporan, w.created_at,
|
||||
k.nama AS kelurahan, k.kecamatan,
|
||||
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.warga_id = w.id) AS total_bantuan,
|
||||
(SELECT COALESCE(SUM(hb2.nominal),0) FROM histori_bantuan hb2 WHERE hb2.warga_id = w.id) AS total_nominal_bantuan
|
||||
FROM warga w
|
||||
LEFT JOIN kelurahan k ON w.kelurahan_id = k.id;
|
||||
|
||||
CREATE OR REPLACE VIEW v_laporan_lengkap AS
|
||||
SELECT
|
||||
l.id, l.no_laporan, l.nama_pelapor, l.hp_pelapor,
|
||||
l.nama_dilaporkan, l.nik, l.kategori, l.jumlah_anggota,
|
||||
l.penghasilan, l.keterangan, l.lat, l.lng, l.status,
|
||||
l.tgl_laporan, l.created_at,
|
||||
k.nama AS kelurahan, k.kecamatan
|
||||
FROM laporan l
|
||||
LEFT JOIN kelurahan k ON l.kelurahan_id = k.id;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: fasilitas_umum (opsional)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS fasilitas_umum (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
kategori VARCHAR(50) NOT NULL,
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
alamat TEXT,
|
||||
keterangan TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: fasilitas_umum
|
||||
-- ============================================================
|
||||
INSERT INTO fasilitas_umum (nama, kategori, lat, lng, alamat, keterangan) VALUES
|
||||
-- Kesehatan
|
||||
('RSUD Dr. Soedarso', 'kesehatan', -0.0461000, 109.3458000, 'Jl. Dr. Soedarso, Pontianak Tenggara', 'Rumah Sakit Umum Daerah Provinsi Kalbar'),
|
||||
('Puskesmas Parit Tokaya', 'kesehatan', -0.0396000, 109.3364000, 'Jl. Paris II, Pontianak Selatan', 'Pelayanan Kesehatan Tingkat Pertama'),
|
||||
('RS Katolik Antonius', 'kesehatan', -0.0336000, 109.3242000, 'Jl. K.H. Wahid Hasyim, Pontianak Kota', 'Rumah Sakit Swasta Tipe B'),
|
||||
-- Pendidikan
|
||||
('Universitas Tanjungpura', 'pendidikan', -0.0622000, 109.3468000, 'Jl. Prof. Dr. H. Hadari Nawawi, Pontianak Tenggara', 'Perguruan Tinggi Negeri Utama Kalbar'),
|
||||
('SMA Negeri 1 Pontianak', 'pendidikan', -0.0354000, 109.3328000, 'Jl. Anjungan, Pontianak Kota', 'Sekolah Menengah Atas Negeri'),
|
||||
('SMP Negeri 1 Pontianak', 'pendidikan', -0.0248000, 109.3344000, 'Jl. Jend. Urip Sumoharjo, Pontianak Kota', 'Sekolah Menengah Pertama Negeri'),
|
||||
-- Pasar Tradisional
|
||||
('Pasar Flamboyan', 'pasar', -0.0381000, 109.3394000, 'Jl. Pahlawan, Pontianak Selatan', 'Pasar Tradisional Terbesar di Kota Pontianak'),
|
||||
('Pasar Mawar', 'pasar', -0.0279000, 109.3387000, 'Jl. Cokroaminoto, Pontianak Kota', 'Pasar Tradisional Sayur & Kebutuhan Pokok'),
|
||||
('Pasar Kemuning', 'pasar', -0.0456000, 109.3218000, 'Jl. Prof. M. Yamin, Pontianak Kota', 'Pasar Rakyat Modern Kelurahan Kota Baru'),
|
||||
-- Kantor Kelurahan
|
||||
('Kantor Lurah Akcaya', 'kelurahan', -0.0352000, 109.3236000, 'Jl. Sutan Syahrir, Pontianak Selatan', 'Pelayanan Publik Kelurahan Akcaya'),
|
||||
('Kantor Lurah Parit Tokaya', 'kelurahan', -0.0478000, 109.3415000, 'Jl. Paris I, Pontianak Selatan', 'Pelayanan Publik Kelurahan Parit Tokaya'),
|
||||
('Kantor Lurah Benua Melayu Darat', 'kelurahan', -0.0345000, 109.3444000, 'Jl. Gajah Mada, Pontianak Selatan', 'Pelayanan Publik Kelurahan BMD');
|
||||
@@ -0,0 +1,876 @@
|
||||
<?php
|
||||
// Cek session — kalau sudah login, langsung ke app
|
||||
session_start();
|
||||
if (!empty($_SESSION['user'])) {
|
||||
header('Location: sig1.html');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GeoSosial — Sistem Informasi Kemiskinan Kota Pontianak</title>
|
||||
<meta name="description" content="Sistem Informasi Geografis kemiskinan dan sosial Kota Pontianak">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--blue: #1a5fff;
|
||||
--blue2: #0f3cc7;
|
||||
--teal: #00c9aa;
|
||||
--green: #10b981;
|
||||
--red: #f03e5a;
|
||||
--ink: #0b1120;
|
||||
--ink2: #3d4b66;
|
||||
--ink3: #8a97ab;
|
||||
--paper: #f0f4fb;
|
||||
--w: #ffffff;
|
||||
--bdr: rgba(11,17,32,.09);
|
||||
}
|
||||
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex; height: 100vh;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* ══════════════ LEFT: MAP ══════════════ */
|
||||
.map-side {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#landingMap {
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
/* Map overlay gradient on right edge (blends into login panel) */
|
||||
.map-side::after {
|
||||
content: '';
|
||||
position: absolute; top: 0; right: 0; bottom: 0;
|
||||
width: 80px;
|
||||
background: linear-gradient(to right, transparent, var(--paper));
|
||||
pointer-events: none; z-index: 400;
|
||||
}
|
||||
|
||||
/* Branding overlay on map */
|
||||
.map-brand {
|
||||
position: absolute; top: 28px; left: 28px; z-index: 500;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(16px);
|
||||
border-radius: 14px;
|
||||
padding: 10px 16px;
|
||||
box-shadow: 0 4px 24px rgba(11,17,32,.12);
|
||||
border: 1px solid var(--bdr);
|
||||
}
|
||||
.map-brand-ic {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--blue), var(--teal));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
.map-brand-name {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 17px; font-weight: 700;
|
||||
color: var(--ink); letter-spacing: -.3px;
|
||||
}
|
||||
.map-brand-sub { font-size: 11px; color: var(--ink3); margin-top: 1px; }
|
||||
|
||||
/* Stats overlay on map bottom */
|
||||
.map-stats {
|
||||
position: absolute; bottom: 28px; left: 28px; z-index: 500;
|
||||
display: flex; gap: 10px;
|
||||
}
|
||||
.mstat {
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: 12px;
|
||||
padding: 10px 16px;
|
||||
box-shadow: 0 4px 20px rgba(11,17,32,.1);
|
||||
min-width: 100px;
|
||||
}
|
||||
.mstat-n { font-size: 10px; color: var(--ink3); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.mstat-v {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 20px; font-weight: 700; color: var(--ink);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.mstat-v.red { color: var(--red); }
|
||||
.mstat-v.blue { color: var(--blue); }
|
||||
.mstat-v.teal { color: var(--teal); }
|
||||
|
||||
/* ══════════════ RIGHT: LOGIN ══════════════ */
|
||||
.login-side {
|
||||
width: 420px;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 48px 44px;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Subtle dot pattern */
|
||||
.login-side::before {
|
||||
content: '';
|
||||
position: absolute; inset: 0; z-index: 0;
|
||||
background-image: radial-gradient(rgba(26,95,255,.07) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.login-inner { position: relative; z-index: 1; }
|
||||
|
||||
.login-eyebrow {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 12px; border-radius: 99px;
|
||||
background: rgba(26,95,255,.08);
|
||||
border: 1px solid rgba(26,95,255,.15);
|
||||
font-size: 11px; font-weight: 600; color: var(--blue);
|
||||
letter-spacing: .04em;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 32px; font-weight: 700;
|
||||
color: var(--ink); letter-spacing: -.8px;
|
||||
line-height: 1.1; margin-bottom: 10px;
|
||||
}
|
||||
.login-sub { font-size: 13.5px; color: var(--ink3); line-height: 1.65; margin-bottom: 32px; font-weight: 400; }
|
||||
|
||||
/* Form */
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-label {
|
||||
display: block; font-size: 11px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: .1em;
|
||||
color: var(--ink3); margin-bottom: 7px;
|
||||
}
|
||||
.form-input-wrap { position: relative; }
|
||||
.form-input-icon {
|
||||
position: absolute; left: 14px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 15px; pointer-events: none;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 42px 12px 42px;
|
||||
background: var(--w);
|
||||
border: 1.5px solid var(--bdr);
|
||||
border-radius: 11px;
|
||||
font-size: 14px; font-family: 'Inter', sans-serif; font-weight: 400;
|
||||
color: var(--ink); outline: none;
|
||||
transition: border-color .18s, box-shadow .18s;
|
||||
}
|
||||
.form-input:focus {
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 0 0 3px rgba(26,95,255,.1);
|
||||
}
|
||||
.form-input.error { border-color: var(--red); box-shadow: 0 0 0 3px rgba(240,62,90,.08); }
|
||||
.toggle-pw {
|
||||
position: absolute; right: 13px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: var(--ink3); font-size: 14px; padding: 2px;
|
||||
transition: color .15s; line-height: 1;
|
||||
}
|
||||
.toggle-pw:hover { color: var(--blue); }
|
||||
|
||||
.err-box {
|
||||
display: none;
|
||||
padding: 11px 14px; border-radius: 10px;
|
||||
background: rgba(240,62,90,.07);
|
||||
border: 1px solid rgba(240,62,90,.18);
|
||||
font-size: 12px; color: var(--red); font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.err-box.show { display: block; }
|
||||
|
||||
.btn-login {
|
||||
width: 100%; padding: 13px;
|
||||
border: none; border-radius: 11px;
|
||||
background: linear-gradient(135deg, var(--blue) 0%, #4b6fff 100%);
|
||||
color: #fff; font-size: 14px; font-weight: 600;
|
||||
font-family: 'Inter', sans-serif; letter-spacing: .01em;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 20px rgba(26,95,255,.28);
|
||||
transition: opacity .18s, transform .15s;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn-login:hover { opacity: .9; transform: translateY(-1px); }
|
||||
.btn-login:disabled { opacity: .55; cursor: not-allowed; transform: none; }
|
||||
.btn-login .spin {
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
border: 2px solid rgba(255,255,255,.4);
|
||||
border-top-color: #fff;
|
||||
animation: spin .7s linear infinite; display: none;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.login-footer {
|
||||
margin-top: 20px; padding-top: 16px;
|
||||
border-top: 1px solid var(--bdr);
|
||||
font-size: 11px; color: var(--ink3);
|
||||
text-align: center; line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 840px) {
|
||||
body { flex-direction: column; overflow: auto; }
|
||||
html, body { height: auto; }
|
||||
.map-side { height: 45vh; flex: none; }
|
||||
.map-side::after { display: none; }
|
||||
.login-side { width: 100%; padding: 36px 28px; }
|
||||
}
|
||||
|
||||
/* Leaflet custom */
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(11,17,32,.15) !important;
|
||||
border: 1px solid var(--bdr) !important;
|
||||
}
|
||||
.leaflet-popup-tip-container { display: none; }
|
||||
|
||||
/* Button Report Trigger */
|
||||
.btn-report-trigger {
|
||||
width: 100%; padding: 13px;
|
||||
border: 1.5px solid var(--blue); border-radius: 11px;
|
||||
background: transparent;
|
||||
color: var(--blue); font-size: 14px; font-weight: 600;
|
||||
font-family: 'Inter', sans-serif; letter-spacing: .01em;
|
||||
cursor: pointer;
|
||||
transition: background .18s, color .18s, transform .15s;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.btn-report-trigger:hover {
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(26,95,255,.15);
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(11, 17, 32, 0.4);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0; pointer-events: none;
|
||||
transition: opacity .3s ease;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal-overlay.show {
|
||||
opacity: 1; pointer-events: auto;
|
||||
}
|
||||
.modal-box {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 20px;
|
||||
width: 1000px; max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 20px 50px rgba(11, 17, 32, 0.15);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
transform: scale(0.95);
|
||||
transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
.modal-overlay.show .modal-box {
|
||||
transform: scale(1);
|
||||
}
|
||||
.modal-header {
|
||||
padding: 20px 28px;
|
||||
border-bottom: 1px solid var(--bdr);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
.modal-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 20px; font-weight: 700; color: var(--ink);
|
||||
}
|
||||
.modal-close {
|
||||
background: none; border: none; font-size: 24px; cursor: pointer; color: var(--ink3);
|
||||
transition: color .15s; line-height: 1;
|
||||
}
|
||||
.modal-close:hover { color: var(--red); }
|
||||
|
||||
.modal-body {
|
||||
padding: 28px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Two-column layout in desktop */
|
||||
.report-grid {
|
||||
display: grid; grid-template-columns: 1.2fr 1fr; gap: 28px;
|
||||
}
|
||||
|
||||
.report-form-side {
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
}
|
||||
|
||||
.report-map-side {
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
.report-map-label {
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .1em; color: var(--ink3);
|
||||
}
|
||||
.report-map {
|
||||
width: 100%; height: 350px; border-radius: 12px; border: 1.5px solid var(--bdr);
|
||||
}
|
||||
|
||||
.form-row-2 {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
|
||||
}
|
||||
|
||||
/* Success Receipt Card */
|
||||
.success-card {
|
||||
text-align: center; padding: 40px 20px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.success-icon {
|
||||
width: 64px; height: 64px; border-radius: 50%; background: #e6fdf5; border: 2px solid var(--green);
|
||||
display: flex; align-items: center; justify-content: center; font-size: 32px; color: var(--green);
|
||||
animation: scale-up .4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@keyframes scale-up { 0% { transform: scale(0.5); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
|
||||
.success-title { font-family: 'Space Grotesk', sans-serif; font-size: 24px; font-weight: 700; color: var(--ink); }
|
||||
.success-no-card {
|
||||
background: var(--paper); border: 1.5px dashed var(--blue); padding: 12px 24px; border-radius: 12px;
|
||||
font-family: 'Space Grotesk', sans-serif; font-size: 20px; font-weight: 700; color: var(--blue);
|
||||
letter-spacing: 0.05em; margin: 10px 0; display: inline-block;
|
||||
}
|
||||
.success-desc { font-size: 14px; color: var(--ink2); max-width: 450px; line-height: 1.6; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.report-grid { grid-template-columns: 1fr; }
|
||||
.report-map { height: 260px; }
|
||||
.modal-box { max-height: 95vh; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ══ KIRI: PETA ══ -->
|
||||
<div class="map-side">
|
||||
<!-- Branding di atas peta -->
|
||||
<div class="map-brand">
|
||||
<div class="map-brand-ic">📍</div>
|
||||
<div>
|
||||
<div class="map-brand-name">GeoSosial</div>
|
||||
<div class="map-brand-sub">Kota Pontianak</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peta Leaflet -->
|
||||
<div id="landingMap"></div>
|
||||
|
||||
<!-- Stat cards di bawah peta -->
|
||||
<div class="map-stats">
|
||||
<div class="mstat">
|
||||
<div class="mstat-n">Kelurahan</div>
|
||||
<div class="mstat-v blue" id="msKel">24</div>
|
||||
</div>
|
||||
<div class="mstat">
|
||||
<div class="mstat-n">Total Warga</div>
|
||||
<div class="mstat-v" id="msWarga">—</div>
|
||||
</div>
|
||||
<div class="mstat">
|
||||
<div class="mstat-n">Sangat Miskin</div>
|
||||
<div class="mstat-v red" id="msSM">—</div>
|
||||
</div>
|
||||
<div class="mstat">
|
||||
<div class="mstat-n">Total Bantuan</div>
|
||||
<div class="mstat-v teal" id="msBantuan">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ KANAN: LOGIN ══ -->
|
||||
<aside class="login-side">
|
||||
<div class="login-inner">
|
||||
<div class="login-eyebrow">🔐 Portal Petugas</div>
|
||||
<h1 class="login-title">Masuk ke<br>Sistem GeoSosial</h1>
|
||||
<p class="login-sub">Masukkan kredensial Anda untuk mengakses data kemiskinan dan sosial Kota Pontianak.</p>
|
||||
|
||||
<div class="err-box" id="errBox">❌ <span id="errMsg">Username atau password salah.</span></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="liUser">Username</label>
|
||||
<div class="form-input-wrap">
|
||||
<span class="form-input-icon">👤</span>
|
||||
<input class="form-input" id="liUser" type="text"
|
||||
placeholder="Masukkan username" autocomplete="username"
|
||||
onkeydown="if(event.key==='Enter') document.getElementById('liPass').focus()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="liPass">Password</label>
|
||||
<div class="form-input-wrap">
|
||||
<span class="form-input-icon">🔒</span>
|
||||
<input class="form-input" id="liPass" type="password"
|
||||
placeholder="Masukkan password" autocomplete="current-password"
|
||||
onkeydown="if(event.key==='Enter') doLogin()">
|
||||
<button class="toggle-pw" onclick="togglePw()" tabindex="-1" type="button">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-login" id="btnLogin" onclick="doLogin()">
|
||||
<div class="spin" id="loginSpin"></div>
|
||||
<span id="btnTxt">Masuk Sekarang →</span>
|
||||
</button>
|
||||
|
||||
<button class="btn-report-trigger" onclick="openReportModal()">
|
||||
📢 Laporkan Warga / Ajukan Bantuan
|
||||
</button>
|
||||
|
||||
<div class="login-footer">
|
||||
Sistem Informasi Geografis Sosial<br>
|
||||
Dinas Sosial Kota Pontianak © 2025
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ══ MODAL LAPORAN WARGA ══ -->
|
||||
<div class="modal-overlay" id="reportModal">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">📢 Formulir Pengaduan & Laporan Warga</h2>
|
||||
<button class="modal-close" onclick="closeReportModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Form Input State -->
|
||||
<div id="reportFormState">
|
||||
<div class="report-grid">
|
||||
<!-- Form fields side -->
|
||||
<div class="report-form-side">
|
||||
<div class="err-box" id="repErrBox">❌ <span id="repErrMsg">Lengkapi kolom wajib diisi.</span></div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repPelapor">Nama Pelapor (Anda)</label>
|
||||
<input class="form-input" id="repPelapor" type="text" placeholder="Nama lengkap Anda">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repHP">No. HP Pelapor</label>
|
||||
<input class="form-input" id="repHP" type="text" placeholder="Contoh: 0812xxxx">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repNama">Warga yang Dilaporkan *</label>
|
||||
<input class="form-input" id="repNama" type="text" placeholder="Nama kepala keluarga" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repNIK">NIK Terlapor (16 Digit)</label>
|
||||
<input class="form-input" id="repNIK" type="text" maxlength="16" placeholder="Nomor Induk Kependudukan">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repKelurahan">Kelurahan Rumah Terlapor *</label>
|
||||
<select class="form-input" id="repKelurahan" required style="padding-left: 14px;">
|
||||
<option value="">-- Pilih Kelurahan --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repKategori">Kategori Kemiskinan</label>
|
||||
<select class="form-input" id="repKategori" style="padding-left: 14px;">
|
||||
<option value="sangat_miskin">Sangat Miskin</option>
|
||||
<option value="miskin" selected>Miskin</option>
|
||||
<option value="hampir_miskin">Hampir Miskin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repAnggota">Jumlah Anggota Keluarga</label>
|
||||
<input class="form-input" id="repAnggota" type="number" min="1" value="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repPenghasilan">Estimasi Penghasilan Bulanan</label>
|
||||
<input class="form-input" id="repPenghasilan" type="number" min="0" value="0" placeholder="Rp">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repKeterangan">Keterangan Kondisi Rumah / Keluarga *</label>
|
||||
<textarea class="form-input" id="repKeterangan" rows="3" placeholder="Jelaskan alasan mengapa warga ini layak menerima bantuan sosial..." style="resize: none; padding-left: 14px;" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repLat">Latitude *</label>
|
||||
<input class="form-input" id="repLat" type="text" readonly placeholder="Klik peta di kanan" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="repLng">Longitude *</label>
|
||||
<input class="form-input" id="repLng" type="text" readonly placeholder="Klik peta di kanan" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-login" id="btnSubmitReport" onclick="submitReport()" style="margin-top: 10px;">
|
||||
<div class="spin" id="reportSpin" style="display: none;"></div>
|
||||
<span id="btnRepTxt">Kirim Laporan Pengaduan</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mini-map side -->
|
||||
<div class="report-map-side">
|
||||
<div class="report-map-label">📍 Tentukan Lokasi Rumah Terlapor (Klik pada peta) *</div>
|
||||
<div class="report-map" id="reportMiniMap"></div>
|
||||
<div style="font-size: 11px; color: var(--ink3); line-height: 1.4;">
|
||||
* Cari dan perbesar peta mini di atas, kemudian **klik pada lokasi rumah** warga yang dilaporkan untuk mendapatkan koordinat Lat/Lng secara tepat.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Receipt State -->
|
||||
<div id="reportSuccessState" style="display: none;">
|
||||
<div class="success-card">
|
||||
<div class="success-icon">✓</div>
|
||||
<h3 class="success-title">Laporan Berhasil Terkirim!</h3>
|
||||
<div class="success-no-card" id="successReportNo">RPT-2026-000</div>
|
||||
<p class="success-desc">
|
||||
Terima kasih atas kepedulian Anda. Laporan pengaduan telah berhasil disimpan ke database. Nomor laporan di atas adalah bukti pengajuan Anda. Admin/Petugas Dinas Sosial akan segera memverifikasi laporan ini ke lapangan.
|
||||
</p>
|
||||
<button class="btn-login" onclick="closeReportModal()" style="width: auto; padding: 12px 32px; margin-top: 15px;">
|
||||
Selesai & Tutup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
// ── Init Peta ────────────────────────────────────────────
|
||||
const map = L.map('landingMap', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
zoomControl: false,
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
|
||||
// Warna per kategori
|
||||
const KAT_COLOR = { sangat_miskin: '#f03e5a', miskin: '#f97316', hampir_miskin: '#f59e0b' };
|
||||
const KAT_LABEL = { sangat_miskin: 'Sangat Miskin', miskin: 'Miskin', hampir_miskin: 'Hampir Miskin' };
|
||||
|
||||
function makeIcon(kat) {
|
||||
const c = KAT_COLOR[kat] || '#8a97ab';
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="36" viewBox="0 0 28 36">
|
||||
<ellipse cx="14" cy="34" rx="6" ry="2" fill="rgba(0,0,0,.18)"/>
|
||||
<path d="M14 2C8.48 2 4 6.48 4 12c0 7.5 10 22 10 22s10-14.5 10-22c0-5.52-4.48-10-10-10z"
|
||||
fill="${c}" stroke="#fff" stroke-width="1.5"/>
|
||||
<circle cx="14" cy="12" r="4" fill="rgba(255,255,255,.9)"/>
|
||||
</svg>`;
|
||||
return L.divIcon({
|
||||
html: svg, className: '', iconSize: [28, 36], iconAnchor: [14, 36], popupAnchor: [0, -38]
|
||||
});
|
||||
}
|
||||
|
||||
// Load data publik (dashboard stats + markers)
|
||||
fetch('api/dashboard.php')
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
if (!j.success) return;
|
||||
const d = j.data;
|
||||
|
||||
// Update stat cards — gunakan d.kpis (field yang benar dari dashboard.php)
|
||||
const kpis = d.kpis || {};
|
||||
const tot = (kpis.total_kk || 0);
|
||||
document.getElementById('msWarga').textContent = tot.toLocaleString('id-ID');
|
||||
document.getElementById('msSM').textContent = (kpis.sangat_miskin || 0).toLocaleString('id-ID');
|
||||
const totBantuan = (d.ranking || []).reduce((s, r) => s + (parseInt(r.total_bantuan) || 0), 0);
|
||||
document.getElementById('msBantuan').textContent =
|
||||
totBantuan >= 1e6
|
||||
? 'Rp ' + (totBantuan / 1e6).toFixed(0) + ' Jt'
|
||||
: 'Rp ' + totBantuan.toLocaleString('id-ID');
|
||||
|
||||
|
||||
// Plot marker per kelurahan (bukan warga individual — hanya titik tengah kelurahan)
|
||||
if (d.ranking) {
|
||||
d.ranking.forEach(r => {
|
||||
if (!r.lat || !r.lng) return;
|
||||
const kat = r.sangat_miskin >= r.miskin ? 'sangat_miskin' : 'miskin';
|
||||
L.marker([parseFloat(r.lat), parseFloat(r.lng)], { icon: makeIcon(kat) })
|
||||
.addTo(map)
|
||||
.bindPopup(`
|
||||
<div style="font-family:'Plus Jakarta Sans',sans-serif;padding:4px 2px;min-width:160px">
|
||||
<div style="font-weight:800;font-size:14px;color:#0b1120">${r.kelurahan}</div>
|
||||
<div style="font-size:11px;color:#8a97ab;margin-bottom:8px">${r.kecamatan}</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
||||
<div style="background:#fde8ec;border-radius:8px;padding:6px 8px;text-align:center">
|
||||
<div style="font-size:9px;color:#f03e5a;font-weight:700">SANGAT MISKIN</div>
|
||||
<div style="font-size:16px;font-weight:800;color:#f03e5a">${r.sangat_miskin}</div>
|
||||
</div>
|
||||
<div style="background:#fff3e8;border-radius:8px;padding:6px 8px;text-align:center">
|
||||
<div style="font-size:9px;color:#f97316;font-weight:700">MISKIN</div>
|
||||
<div style="font-size:16px;font-weight:800;color:#f97316">${r.miskin}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:8px;font-size:11px;color:#3d4b66">
|
||||
Total: <strong>${r.total}</strong> KK • Bantuan: <strong style="color:#00c9aa">Rp ${parseInt(r.total_bantuan||0).toLocaleString('id-ID')}</strong>
|
||||
</div>
|
||||
</div>`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// ── Login ────────────────────────────────────────────────
|
||||
function togglePw() {
|
||||
|
||||
const inp = document.getElementById('liPass');
|
||||
const btn = document.querySelector('.toggle-pw');
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
btn.textContent = inp.type === 'password' ? '👁' : '🙈';
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
const u = document.getElementById('liUser').value.trim();
|
||||
const p = document.getElementById('liPass').value;
|
||||
const eb = document.getElementById('errBox');
|
||||
const btn = document.getElementById('btnLogin');
|
||||
const spin = document.getElementById('loginSpin');
|
||||
const txt = document.getElementById('btnTxt');
|
||||
|
||||
eb.classList.remove('show');
|
||||
document.getElementById('liUser').classList.remove('error');
|
||||
document.getElementById('liPass').classList.remove('error');
|
||||
|
||||
if (!u || !p) {
|
||||
document.getElementById('errMsg').textContent = 'Username dan password wajib diisi.';
|
||||
eb.classList.add('show');
|
||||
if (!u) document.getElementById('liUser').classList.add('error');
|
||||
if (!p) document.getElementById('liPass').classList.add('error');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
spin.style.display = 'block';
|
||||
txt.textContent = 'Memverifikasi...';
|
||||
|
||||
try {
|
||||
const res = await fetch('api/auth.php?action=login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: u, password: p })
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
txt.textContent = '✓ Berhasil! Mengalihkan...';
|
||||
setTimeout(() => { location.href = 'sig1.html'; }, 400);
|
||||
} else {
|
||||
document.getElementById('errMsg').textContent = json.message || 'Username atau password salah.';
|
||||
eb.classList.add('show');
|
||||
document.getElementById('liUser').classList.add('error');
|
||||
document.getElementById('liPass').classList.add('error');
|
||||
btn.disabled = false;
|
||||
spin.style.display = 'none';
|
||||
txt.textContent = 'Masuk Sekarang →';
|
||||
}
|
||||
} catch {
|
||||
document.getElementById('errMsg').textContent = 'Koneksi server gagal. Coba lagi.';
|
||||
eb.classList.add('show');
|
||||
btn.disabled = false;
|
||||
spin.style.display = 'none';
|
||||
txt.textContent = 'Masuk Sekarang →';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Portal Pengaduan Publik (Laporan Warga) ───────────────
|
||||
let reportMap = null;
|
||||
let reportMarker = null;
|
||||
let kelurahanLoaded = false;
|
||||
|
||||
async function openReportModal() {
|
||||
const modal = document.getElementById('reportModal');
|
||||
modal.classList.add('show');
|
||||
|
||||
// Load kelurahan options dynamically
|
||||
if (!kelurahanLoaded) {
|
||||
try {
|
||||
const res = await fetch('api/kelurahan.php');
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
const select = document.getElementById('repKelurahan');
|
||||
json.data.forEach(kel => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = kel.id;
|
||||
opt.textContent = `${kel.nama} (${kel.kecamatan})`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
kelurahanLoaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal memuat data kelurahan: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize/refresh Leaflet mini map inside modal
|
||||
setTimeout(() => {
|
||||
if (!reportMap) {
|
||||
reportMap = L.map('reportMiniMap', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(reportMap);
|
||||
|
||||
reportMap.on('click', function(e) {
|
||||
const lat = e.latlng.lat.toFixed(7);
|
||||
const lng = e.latlng.lng.toFixed(7);
|
||||
document.getElementById('repLat').value = lat;
|
||||
document.getElementById('repLng').value = lng;
|
||||
|
||||
if (reportMarker) {
|
||||
reportMarker.setLatLng(e.latlng);
|
||||
} else {
|
||||
reportMarker = L.marker(e.latlng, { draggable: true }).addTo(reportMap);
|
||||
reportMarker.on('dragend', function(ev) {
|
||||
const pos = reportMarker.getLatLng();
|
||||
document.getElementById('repLat').value = pos.lat.toFixed(7);
|
||||
document.getElementById('repLng').value = pos.lng.toFixed(7);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reportMap.invalidateSize();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function closeReportModal() {
|
||||
document.getElementById('reportModal').classList.remove('show');
|
||||
|
||||
// Reset form fields
|
||||
document.getElementById('repPelapor').value = '';
|
||||
document.getElementById('repHP').value = '';
|
||||
document.getElementById('repNama').value = '';
|
||||
document.getElementById('repNIK').value = '';
|
||||
document.getElementById('repKelurahan').value = '';
|
||||
document.getElementById('repKategori').value = 'miskin';
|
||||
document.getElementById('repAnggota').value = '1';
|
||||
document.getElementById('repPenghasilan').value = '0';
|
||||
document.getElementById('repKeterangan').value = '';
|
||||
document.getElementById('repLat').value = '';
|
||||
document.getElementById('repLng').value = '';
|
||||
|
||||
// Remove marker if exists
|
||||
if (reportMarker) {
|
||||
reportMap.removeLayer(reportMarker);
|
||||
reportMarker = null;
|
||||
}
|
||||
|
||||
// Reset view states
|
||||
document.getElementById('reportFormState').style.display = 'block';
|
||||
document.getElementById('reportSuccessState').style.display = 'none';
|
||||
document.getElementById('repErrBox').classList.remove('show');
|
||||
}
|
||||
|
||||
async function submitReport() {
|
||||
const pelapor = document.getElementById('repPelapor').value.trim();
|
||||
const hp = document.getElementById('repHP').value.trim();
|
||||
const nama = document.getElementById('repNama').value.trim();
|
||||
const nik = document.getElementById('repNIK').value.trim();
|
||||
const kelurahan_id = document.getElementById('repKelurahan').value;
|
||||
const kategori = document.getElementById('repKategori').value;
|
||||
const jumlah_anggota = parseInt(document.getElementById('repAnggota').value) || 1;
|
||||
const penghasilan = parseInt(document.getElementById('repPenghasilan').value) || 0;
|
||||
const keterangan = document.getElementById('repKeterangan').value.trim();
|
||||
const lat = document.getElementById('repLat').value;
|
||||
const lng = document.getElementById('repLng').value;
|
||||
|
||||
const eb = document.getElementById('repErrBox');
|
||||
const errMsg = document.getElementById('repErrMsg');
|
||||
const btn = document.getElementById('btnSubmitReport');
|
||||
const spin = document.getElementById('reportSpin');
|
||||
const txt = document.getElementById('btnRepTxt');
|
||||
|
||||
eb.classList.remove('show');
|
||||
|
||||
// Validation
|
||||
if (!nama || !kelurahan_id || !keterangan || !lat || !lng) {
|
||||
errMsg.textContent = 'Nama warga, kelurahan, keterangan kondisi, dan koordinat peta wajib diisi.';
|
||||
eb.classList.add('show');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nik && nik.length !== 16) {
|
||||
errMsg.textContent = 'NIK harus tepat 16 digit.';
|
||||
eb.classList.add('show');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
spin.style.display = 'inline-block';
|
||||
txt.textContent = 'Mengirim Laporan...';
|
||||
|
||||
try {
|
||||
const res = await fetch('api/laporan.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
pelapor,
|
||||
hp,
|
||||
nama,
|
||||
nik: nik || null,
|
||||
kelurahan_id: parseInt(kelurahan_id),
|
||||
kategori,
|
||||
jumlah_anggota,
|
||||
penghasilan,
|
||||
keterangan,
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng)
|
||||
})
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
document.getElementById('successReportNo').textContent = json.data.no;
|
||||
document.getElementById('reportFormState').style.display = 'none';
|
||||
document.getElementById('reportSuccessState').style.display = 'block';
|
||||
} else {
|
||||
errMsg.textContent = json.message || 'Gagal mengirim laporan. Coba lagi.';
|
||||
eb.classList.add('show');
|
||||
}
|
||||
} catch (e) {
|
||||
errMsg.textContent = 'Koneksi server gagal. Silakan coba lagi.';
|
||||
eb.classList.add('show');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spin.style.display = 'none';
|
||||
txt.textContent = 'Kirim Laporan Pengaduan';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+687
@@ -0,0 +1,687 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil — GeoSosial Pontianak</title>
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #f0f4fb;
|
||||
--blue: #1a5fff;
|
||||
--blue2: #0f3cc7;
|
||||
--teal: #0891b2;
|
||||
--grn: #10b981;
|
||||
--red: #f03e5a;
|
||||
--purple:#7c3aed;
|
||||
--ink: #0b1120;
|
||||
--ink2: #3d4b66;
|
||||
--ink3: #8a97ab;
|
||||
--card: #ffffff;
|
||||
--card-h:#f8faff;
|
||||
--bdr: rgba(11,17,32,.09);
|
||||
--sh: 0 4px 24px rgba(11,17,32,.08);
|
||||
--shm: 0 8px 40px rgba(11,17,32,.12);
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── SUBTLE DOT BG ── */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed; inset: 0; z-index: 0;
|
||||
background-image: radial-gradient(rgba(26,95,255,.06) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── TOPBAR ── */
|
||||
.topbar {
|
||||
position: sticky; top: 0; z-index: 500;
|
||||
height: 54px;
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(20px) saturate(1.8);
|
||||
border-bottom: 1px solid var(--bdr);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 28px;
|
||||
box-shadow: 0 1px 12px rgba(11,17,32,.06);
|
||||
}
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand-logo {
|
||||
width: 28px; height: 28px; border-radius: 7px;
|
||||
background: linear-gradient(135deg, var(--blue), var(--teal));
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px;
|
||||
}
|
||||
.brand-name {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 15px; font-weight: 700; color: var(--ink);
|
||||
}
|
||||
.topbar-right { display: flex; gap: 8px; }
|
||||
.tbtn {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 6px 14px; border-radius: 99px;
|
||||
font-size: 11px; font-weight: 600; font-family: inherit;
|
||||
cursor: pointer; border: none; text-decoration: none;
|
||||
transition: all .18s;
|
||||
}
|
||||
.tbtn.ghost {
|
||||
background: var(--bg); border: 1px solid var(--bdr); color: var(--ink2);
|
||||
}
|
||||
.tbtn.ghost:hover { background: #e8efff; color: var(--blue); border-color: #c0d0ff; }
|
||||
.tbtn.danger {
|
||||
background: #fde8ec; border: 1px solid #f9b8c4; color: var(--red);
|
||||
}
|
||||
.tbtn.danger:hover { background: var(--red); color: #fff; border-color: var(--red); }
|
||||
|
||||
/* ── PAGE WRAP ── */
|
||||
.page { position: relative; z-index: 1; padding: 40px 20px 60px; }
|
||||
|
||||
/* ── PROFILE HEADER ── */
|
||||
.phead {
|
||||
max-width: 700px; margin: 0 auto 28px;
|
||||
display: flex; align-items: center; gap: 24px;
|
||||
padding: 28px 32px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shm);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
/* Colored top strip */
|
||||
.phead::before {
|
||||
content: '';
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 4px;
|
||||
background: linear-gradient(90deg, var(--blue), var(--teal), var(--purple));
|
||||
}
|
||||
/* Avatar */
|
||||
.av-wrap { position: relative; flex-shrink: 0; }
|
||||
.av {
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 28px; font-weight: 700; color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
.av::before {
|
||||
content: '';
|
||||
position: absolute; inset: -3px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--blue), var(--teal));
|
||||
z-index: -1;
|
||||
}
|
||||
.av::after {
|
||||
content: '';
|
||||
position: absolute; inset: -7px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(26,95,255,.18), rgba(8,145,178,.18));
|
||||
z-index: -2;
|
||||
animation: spin-ring 8s linear infinite;
|
||||
}
|
||||
@keyframes spin-ring {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.av-pulse {
|
||||
position: absolute; bottom: 2px; right: 2px;
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: var(--grn);
|
||||
border: 2.5px solid #fff;
|
||||
}
|
||||
.av-pulse::after {
|
||||
content: '';
|
||||
position: absolute; inset: -3px; border-radius: 50%;
|
||||
background: rgba(16,185,129,.3);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse { 0%{transform:scale(1);opacity:1} 100%{transform:scale(2.2);opacity:0} }
|
||||
|
||||
.phead-meta { flex: 1; }
|
||||
.phead-name {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 22px; font-weight: 700; color: var(--ink);
|
||||
letter-spacing: -.4px;
|
||||
}
|
||||
.phead-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 10px; border-radius: 99px;
|
||||
font-size: 11px; font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.chip.role-admin { background: #fde8ec; color: var(--red); border-color: #f9b8c4; }
|
||||
.chip.role-petugas { background: #e8efff; color: var(--blue); border-color: #c0d0ff; }
|
||||
.chip.role-wali { background: #f0ebff; color: var(--purple); border-color: #d4bfff; }
|
||||
.chip.role-viewer { background: var(--bg); color: var(--ink3); border-color: var(--bdr); }
|
||||
.chip.online { background: #e5f8f1; color: var(--grn); border-color: #a7f3d0; }
|
||||
.phead-uname { font-size: 12px; color: var(--ink3); margin-top: 5px; }
|
||||
|
||||
/* ── MAIN LAYOUT ── */
|
||||
.main {
|
||||
max-width: 700px; margin: 0 auto;
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
}
|
||||
|
||||
/* ── GLASS CARD → LIGHT CARD ── */
|
||||
.gcard {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--sh);
|
||||
transition: box-shadow .2s;
|
||||
}
|
||||
.gcard:hover { box-shadow: var(--shm); }
|
||||
|
||||
/* ── SECTION HEADER ── */
|
||||
.sec-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid var(--bdr);
|
||||
background: var(--card);
|
||||
}
|
||||
.sec-left { display: flex; align-items: center; gap: 12px; }
|
||||
.sec-ic {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 17px;
|
||||
}
|
||||
.sec-ic.c { background: #e8efff; }
|
||||
.sec-ic.p { background: #fef3c7; }
|
||||
.sec-ic.s { background: #f0ebff; }
|
||||
.sec-title { font-size: 14px; font-weight: 700; color: var(--ink); }
|
||||
.sec-sub { font-size: 11px; color: var(--ink3); margin-top: 1px; }
|
||||
|
||||
/* Edit button */
|
||||
.ebtn {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 6px 14px; border-radius: 8px;
|
||||
font-size: 11px; font-weight: 700; font-family: inherit;
|
||||
background: var(--bg); color: var(--ink2);
|
||||
border: 1px solid var(--bdr); cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.ebtn:hover { background: #e8efff; color: var(--blue); border-color: #c0d0ff; }
|
||||
.ebtn.on { background: #e8efff; color: var(--blue); border-color: #c0d0ff; }
|
||||
|
||||
/* ── INFO ROWS ── */
|
||||
.irows { padding: 12px 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.irow {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
||||
}
|
||||
.icell {
|
||||
padding: 14px 18px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.ilbl {
|
||||
font-size: 9px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .1em; color: var(--ink3); margin-bottom: 7px;
|
||||
}
|
||||
.ival {
|
||||
font-size: 13px; font-weight: 600; color: var(--ink);
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.ival.mono { font-family: 'Space Grotesk', monospace; }
|
||||
.ival .tag {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 3px 9px; border-radius: 99px;
|
||||
font-size: 11px; font-weight: 700;
|
||||
}
|
||||
|
||||
/* inputs */
|
||||
.gi {
|
||||
width: 100%; padding: 9px 12px;
|
||||
background: #fff;
|
||||
border: 1.5px solid var(--bdr);
|
||||
border-radius: 9px; font-size: 13px;
|
||||
font-family: inherit; color: var(--ink); outline: none;
|
||||
transition: border-color .18s;
|
||||
}
|
||||
.gi:focus { border-color: var(--blue); }
|
||||
|
||||
/* Save bar */
|
||||
.save-bar {
|
||||
display: none; justify-content: flex-end; gap: 10px;
|
||||
padding: 14px 22px; border-top: 1px solid var(--bdr);
|
||||
background: var(--bg);
|
||||
}
|
||||
.save-bar.show { display: flex; }
|
||||
.sbtn-c {
|
||||
padding: 9px 18px; border-radius: 9px; font-size: 12px;
|
||||
font-weight: 600; font-family: inherit; cursor: pointer;
|
||||
background: #fff; border: 1px solid var(--bdr); color: var(--ink2);
|
||||
transition: all .15s;
|
||||
}
|
||||
.sbtn-c:hover { background: var(--bg); }
|
||||
.sbtn-s {
|
||||
padding: 9px 20px; border-radius: 9px; font-size: 12px;
|
||||
font-weight: 700; font-family: inherit; cursor: pointer;
|
||||
background: linear-gradient(135deg, var(--blue), var(--teal)); border: none; color: #fff;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.sbtn-s:hover { opacity: .85; }
|
||||
|
||||
/* ── PASSWORD ── */
|
||||
.pfields { padding: 20px 22px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.pf > label {
|
||||
display: block; font-size: 9px; font-weight: 700;
|
||||
text-transform: uppercase; letter-spacing: .1em;
|
||||
color: var(--ink3); margin-bottom: 7px;
|
||||
}
|
||||
.pf > label span { font-size: 9px; font-weight: 400; text-transform: none; letter-spacing: 0; }
|
||||
.pwrap { position: relative; }
|
||||
.pwrap input {
|
||||
width: 100%; padding: 11px 40px 11px 40px;
|
||||
background: var(--bg);
|
||||
border: 1.5px solid var(--bdr);
|
||||
border-radius: 10px; font-size: 13px;
|
||||
font-family: inherit; color: var(--ink); outline: none;
|
||||
transition: border-color .18s, background .18s;
|
||||
}
|
||||
.pwrap input:focus { border-color: var(--blue); background: #fff; }
|
||||
.pwrap input::placeholder { color: var(--ink3); }
|
||||
.ppfx {
|
||||
position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
|
||||
font-size: 14px; pointer-events: none; user-select: none;
|
||||
}
|
||||
.ptgl {
|
||||
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer; color: var(--ink3);
|
||||
font-size: 14px; padding: 2px; line-height: 1; transition: color .15s;
|
||||
}
|
||||
.ptgl:hover { color: var(--blue); }
|
||||
.sbar { height: 3px; border-radius: 99px; background: var(--bdr); margin-top: 7px; overflow: hidden; }
|
||||
.sfill { height: 100%; width: 0; border-radius: 99px; transition: width .3s, background .3s; }
|
||||
.shint { font-size: 10px; margin-top: 4px; color: var(--ink3); }
|
||||
.pact { display: flex; justify-content: flex-end; padding-top: 4px; }
|
||||
.pbtn {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 11px 28px; border-radius: 10px; border: none;
|
||||
background: linear-gradient(135deg, var(--blue) 0%, var(--teal) 100%);
|
||||
color: #fff; font-size: 12px; font-weight: 700;
|
||||
font-family: inherit; cursor: pointer;
|
||||
box-shadow: 0 4px 20px rgba(26,95,255,.22);
|
||||
transition: opacity .18s, transform .15s;
|
||||
}
|
||||
.pbtn:hover { opacity: .88; transform: translateY(-1px); }
|
||||
.pbtn:disabled { opacity: .45; cursor: not-allowed; transform: none; }
|
||||
|
||||
/* ── STATS ROW ── */
|
||||
.stats {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
|
||||
}
|
||||
.stat {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: 16px; padding: 16px;
|
||||
text-align: center;
|
||||
box-shadow: var(--sh);
|
||||
transition: box-shadow .2s, transform .2s;
|
||||
}
|
||||
.stat:hover { box-shadow: var(--shm); transform: translateY(-2px); }
|
||||
.stat-ic { font-size: 22px; margin-bottom: 8px; }
|
||||
.stat-n { font-size: 11px; color: var(--ink3); margin-bottom: 3px; }
|
||||
.stat-v { font-size: 13px; font-weight: 700; color: var(--ink); }
|
||||
|
||||
/* ── TOAST ── */
|
||||
#toast {
|
||||
position: fixed; bottom: 28px; left: 50%;
|
||||
transform: translateX(-50%) translateY(90px);
|
||||
padding: 12px 22px; border-radius: 99px;
|
||||
background: var(--ink);
|
||||
color: #fff; font-size: 12px; font-weight: 700;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,.2);
|
||||
transition: transform .35s cubic-bezier(.34,1.56,.64,1);
|
||||
z-index: 9999; white-space: nowrap;
|
||||
}
|
||||
#toast.show { transform: translateX(-50%) translateY(0); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.phead { flex-direction: column; text-align: center; align-items: center; }
|
||||
.phead-row { justify-content: center; }
|
||||
.irow { grid-template-columns: 1fr; }
|
||||
.icell + .icell { border-left: none; border-top: 1px solid var(--bdr); }
|
||||
.stats { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<nav class="topbar">
|
||||
<a class="brand" href="sig1.html">
|
||||
<div class="brand-logo">📍</div>
|
||||
<span class="brand-name">GeoSosial</span>
|
||||
</a>
|
||||
<div class="topbar-right">
|
||||
<a class="tbtn ghost" href="sig1.html">← Peta</a>
|
||||
<button class="tbtn danger" onclick="doLogout()">❤ Keluar</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- PROFILE HEADER -->
|
||||
<div class="phead">
|
||||
<div class="av-wrap">
|
||||
<div class="av" id="avEl" style="background:#0d2340">
|
||||
<span id="avIni">AD</span>
|
||||
</div>
|
||||
<div class="av-pulse"></div>
|
||||
</div>
|
||||
<div class="phead-meta">
|
||||
<div class="phead-name" id="phName">—</div>
|
||||
<div class="phead-row">
|
||||
<span class="chip" id="phRole">—</span>
|
||||
<span class="chip online">● Online</span>
|
||||
</div>
|
||||
<div class="phead-uname" id="phUser">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STATS ROW -->
|
||||
<div class="stats main" style="padding: 0; margin-bottom: 0; display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; max-width: 700px;">
|
||||
<div class="stat">
|
||||
<div class="stat-ic">🔐</div>
|
||||
<div class="stat-n">Hak Akses</div>
|
||||
<div class="stat-v" id="stAccess">—</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-ic">🗓</div>
|
||||
<div class="stat-n">Bergabung</div>
|
||||
<div class="stat-v" id="stJoin">—</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-ic">⏱</div>
|
||||
<div class="stat-n">Login Terakhir</div>
|
||||
<div class="stat-v" id="stLogin">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN CARDS -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Informasi Akun -->
|
||||
<div class="gcard" id="secInfo">
|
||||
<div class="sec-head">
|
||||
<div class="sec-left">
|
||||
<div class="sec-ic c">🪪</div>
|
||||
<div>
|
||||
<div class="sec-title">Informasi Akun</div>
|
||||
<div class="sec-sub">Data profil dan pengaturan akun</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ebtn" id="editBtn" onclick="toggleEdit()">✏️ Edit</button>
|
||||
</div>
|
||||
<div class="irows">
|
||||
<div class="irow">
|
||||
<div class="icell">
|
||||
<div class="ilbl">Username</div>
|
||||
<div class="ival mono">@ <span id="dUser">—</span></div>
|
||||
</div>
|
||||
<div class="icell">
|
||||
<div class="ilbl">Nama Lengkap / Jabatan</div>
|
||||
<div class="ival" id="dJabW"><span id="dJab">—</span></div>
|
||||
<input class="gi" id="eJab" style="display:none" placeholder="cth. Kadis Sosial">
|
||||
</div>
|
||||
</div>
|
||||
<div class="irow">
|
||||
<div class="icell">
|
||||
<div class="ilbl">Role</div>
|
||||
<div class="ival" id="dRoleW"><span class="tag" id="dRole">—</span></div>
|
||||
<select class="gi" id="eRole" style="display:none">
|
||||
<option value="admin">🛡 Admin</option>
|
||||
<option value="petugas">👮 Petugas</option>
|
||||
<option value="walikota">🏛️ Walikota</option>
|
||||
<option value="viewer">👁️ Viewer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icell">
|
||||
<div class="ilbl">Status Akun</div>
|
||||
<div class="ival"><span id="dStatus" style="color:var(--grn);font-weight:700">● Aktif</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="irow">
|
||||
<div class="icell">
|
||||
<div class="ilbl">Sesi Aktif</div>
|
||||
<div class="ival" style="color:var(--grn);font-weight:700">● Online</div>
|
||||
</div>
|
||||
<div class="icell">
|
||||
<div class="ilbl">Login Terakhir</div>
|
||||
<div class="ival" id="dLogin">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-bar" id="saveBar">
|
||||
<button class="sbtn-c" onclick="cancelEdit()">Batal</button>
|
||||
<button class="sbtn-s" onclick="saveInfo()">💾 Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ganti Password -->
|
||||
<div class="gcard" id="secPw">
|
||||
<div class="sec-head">
|
||||
<div class="sec-left">
|
||||
<div class="sec-ic p">🔑</div>
|
||||
<div>
|
||||
<div class="sec-title">Ganti Password</div>
|
||||
<div class="sec-sub">Perbarui kata sandi akun Anda</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pfields">
|
||||
<div class="pf">
|
||||
<label>Password Lama</label>
|
||||
<div class="pwrap">
|
||||
<span class="ppfx">🔒</span>
|
||||
<input type="password" id="pwOld" placeholder="Masukkan password lama">
|
||||
<button class="ptgl" onclick="togglePw('pwOld',this)" tabindex="-1">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf">
|
||||
<label>Password Baru <span>(min. 6 karakter)</span></label>
|
||||
<div class="pwrap">
|
||||
<span class="ppfx">🔑</span>
|
||||
<input type="password" id="pwNew" placeholder="Masukkan password baru" oninput="checkStr(this.value)">
|
||||
<button class="ptgl" onclick="togglePw('pwNew',this)" tabindex="-1">👁</button>
|
||||
</div>
|
||||
<div class="sbar"><div class="sfill" id="sfill"></div></div>
|
||||
<div class="shint" id="shint">—</div>
|
||||
</div>
|
||||
<div class="pf">
|
||||
<label>Konfirmasi Password</label>
|
||||
<div class="pwrap">
|
||||
<span class="ppfx">✅</span>
|
||||
<input type="password" id="pwCfm" placeholder="Ulangi password baru">
|
||||
<button class="ptgl" onclick="togglePw('pwCfm',this)" tabindex="-1">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pact">
|
||||
<button class="pbtn" id="btnPw" onclick="savePw()">🔒 Simpan Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script>
|
||||
let PD = null, editing = false, ttimer;
|
||||
|
||||
const RM = {
|
||||
admin: { cls:'role-admin', lbl:'🛡 Admin', bg:'rgba(248,113,113,.12)', fg:'#fca5a5' },
|
||||
petugas: { cls:'role-petugas', lbl:'👮 Petugas', bg:'rgba(34,211,238,.1)', fg:'#22d3ee' },
|
||||
walikota: { cls:'role-wali', lbl:'🏛️ Walikota', bg:'rgba(167,139,250,.12)', fg:'#c4b5fd' },
|
||||
viewer: { cls:'role-viewer', lbl:'👁️ Viewer', bg:'rgba(148,163,184,.1)', fg:'#94a3b8' },
|
||||
};
|
||||
const AVC = ['#1a5fff','#7c3aed','#0891b2','#059669','#dc2626','#b45309'];
|
||||
|
||||
async function loadProfile() {
|
||||
let base = null;
|
||||
try {
|
||||
const r = await fetch('api/auth.php?action=check');
|
||||
const j = await r.json();
|
||||
if (!j.success) { location.href = 'sig1.html'; return; }
|
||||
base = j.data;
|
||||
} catch { location.href = 'sig1.html'; return; }
|
||||
|
||||
let full = { ...base };
|
||||
try {
|
||||
const r2 = await fetch('api/users.php');
|
||||
const j2 = await r2.json();
|
||||
if (j2.success) {
|
||||
const found = j2.data.find(x => x.id == base.id);
|
||||
if (found) full = { ...base, ...found };
|
||||
}
|
||||
} catch {}
|
||||
|
||||
PD = full;
|
||||
try { render(full); } catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function render(u) {
|
||||
const rm = RM[u.role] || RM.viewer;
|
||||
const ini = (u.username || 'U').substring(0, 2).toUpperCase();
|
||||
const color = AVC[(u.id || 0) % AVC.length];
|
||||
const now = new Date().toLocaleString('id-ID', {day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||
const joined = u.created_at ? new Date(u.created_at).toLocaleDateString('id-ID',{day:'2-digit',month:'short',year:'numeric'}) : '—';
|
||||
|
||||
// Avatar
|
||||
const av = $('avEl'); if (av) av.style.background = color;
|
||||
const ai = $('avIni'); if (ai) ai.textContent = ini;
|
||||
|
||||
// Header
|
||||
if ($('phName')) $('phName').textContent = u.jabatan || u.username;
|
||||
const pr = $('phRole');
|
||||
if (pr) { pr.textContent = rm.lbl; pr.className = 'chip ' + rm.cls; }
|
||||
if ($('phUser')) $('phUser').textContent = '@ ' + u.username;
|
||||
|
||||
// Stats
|
||||
if ($('stAccess')) $('stAccess').textContent = u.can_edit ? 'Baca & Tulis' : 'Baca Saja';
|
||||
if ($('stJoin')) $('stJoin').textContent = joined;
|
||||
if ($('stLogin')) $('stLogin').textContent = now;
|
||||
|
||||
// Info card
|
||||
if ($('dUser')) $('dUser').textContent = u.username;
|
||||
if ($('dJab')) $('dJab').textContent = u.jabatan || '—';
|
||||
if ($('eJab')) $('eJab').value = u.jabatan || '';
|
||||
if ($('eRole')) $('eRole').value = u.role;
|
||||
const dr = $('dRole');
|
||||
if (dr) { dr.textContent = rm.lbl; dr.style.background = rm.bg; dr.style.color = rm.fg; }
|
||||
if ($('dStatus')) {
|
||||
$('dStatus').textContent = u.aktif ? '● Aktif' : '○ Nonaktif';
|
||||
$('dStatus').style.color = u.aktif ? 'var(--grn)' : 'var(--ink3)';
|
||||
}
|
||||
if ($('dLogin')) $('dLogin').textContent = now;
|
||||
if ($('eRole')) $('eRole').disabled = !u.is_admin;
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
editing = !editing;
|
||||
$('editBtn').textContent = editing ? '✕ Batal' : '✏️ Edit';
|
||||
$('editBtn').className = 'ebtn' + (editing ? ' on' : '');
|
||||
$('dJabW').style.display = editing ? 'none' : '';
|
||||
$('eJab').style.display = editing ? '' : 'none';
|
||||
$('dRoleW').style.display = editing ? 'none' : '';
|
||||
$('eRole').style.display = editing ? '' : 'none';
|
||||
$('saveBar').classList.toggle('show', editing);
|
||||
if (editing) $('eJab').focus();
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
$('editBtn').textContent = '✏️ Edit'; $('editBtn').className = 'ebtn';
|
||||
$('eJab').value = PD?.jabatan || '';
|
||||
$('eRole').value = PD?.role || 'viewer';
|
||||
$('dJabW').style.display = ''; $('eJab').style.display = 'none';
|
||||
$('dRoleW').style.display = ''; $('eRole').style.display = 'none';
|
||||
$('saveBar').classList.remove('show');
|
||||
}
|
||||
|
||||
async function saveInfo() {
|
||||
const jabatan = $('eJab').value.trim(), role = $('eRole').value;
|
||||
try {
|
||||
const r = await fetch(`api/users.php?id=${PD.id}`, {
|
||||
method:'PUT', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({jabatan, role, can_edit: PD.can_edit, aktif: PD.aktif})
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.success) { PD.jabatan=jabatan; PD.role=role; render(PD); cancelEdit(); toast('✓ Perubahan disimpan'); }
|
||||
else alert('Gagal: ' + j.message);
|
||||
} catch { alert('Koneksi server gagal.'); }
|
||||
}
|
||||
|
||||
function togglePw(id, btn) {
|
||||
const inp = $(id);
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
btn.textContent = inp.type === 'password' ? '👁' : '🙈';
|
||||
}
|
||||
|
||||
function checkStr(v) {
|
||||
const bar = $('sfill'), lbl = $('shint');
|
||||
if (!v) { bar.style.width='0'; lbl.textContent='—'; lbl.style.color='var(--ink3)'; return; }
|
||||
let s=0;
|
||||
if(v.length>=6)s++; if(v.length>=10)s++;
|
||||
if(/[A-Z]/.test(v))s++; if(/[0-9]/.test(v))s++; if(/[^A-Za-z0-9]/.test(v))s++;
|
||||
const ls=[{w:'20%',c:'#f87171',t:'Sangat Lemah'},{w:'40%',c:'#fb923c',t:'Lemah'},{w:'60%',c:'#fbbf24',t:'Cukup'},{w:'80%',c:'#34d399',t:'Kuat'},{w:'100%',c:'#10b981',t:'Sangat Kuat'}];
|
||||
const lv=ls[Math.min(s,4)];
|
||||
bar.style.width=lv.w; bar.style.background=lv.c;
|
||||
lbl.textContent=lv.t; lbl.style.color=lv.c;
|
||||
}
|
||||
|
||||
async function savePw() {
|
||||
const old=$('pwOld').value, nw=$('pwNew').value, cf=$('pwCfm').value;
|
||||
if(!old){alert('Password lama wajib diisi.');return;}
|
||||
if(nw.length<6){alert('Password baru minimal 6 karakter.');return;}
|
||||
if(nw!==cf){alert('Konfirmasi password tidak cocok.');return;}
|
||||
const btn=$('btnPw'); btn.disabled=true; btn.textContent='⏳ Menyimpan...';
|
||||
try {
|
||||
const r=await fetch(`api/users.php?id=${PD.id}`,{
|
||||
method:'PUT',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({jabatan:PD.jabatan,role:PD.role,can_edit:PD.can_edit,aktif:PD.aktif,password:nw})
|
||||
});
|
||||
const j=await r.json();
|
||||
if(j.success){
|
||||
$('pwOld').value=$('pwNew').value=$('pwCfm').value='';
|
||||
$('sfill').style.width='0'; $('shint').textContent='—';
|
||||
toast('🔒 Password berhasil diperbarui');
|
||||
} else alert('Gagal: '+j.message);
|
||||
} catch { alert('Koneksi server gagal.'); }
|
||||
btn.disabled=false; btn.textContent='🔒 Simpan Password';
|
||||
}
|
||||
|
||||
async function doLogout() {
|
||||
try { await fetch('api/auth.php?action=logout',{method:'POST'}); } catch{}
|
||||
location.href='sig1.html';
|
||||
}
|
||||
|
||||
function toast(msg) {
|
||||
$('toast').textContent = msg;
|
||||
$('toast').classList.add('show');
|
||||
clearTimeout(ttimer);
|
||||
ttimer = setTimeout(() => $('toast').classList.remove('show'), 2800);
|
||||
}
|
||||
|
||||
loadProfile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"><title>Test Login</title></head>
|
||||
<body style="background:#111;color:#fff;font-family:sans-serif;padding:30px">
|
||||
<h2>🔧 Test Login GeoSosial</h2>
|
||||
<p>Username: <input id="u" value="admin" style="padding:6px;width:200px"></p>
|
||||
<p>Password: <input id="p" type="password" value="admin123" style="padding:6px;width:200px"></p>
|
||||
<button onclick="go()" style="padding:10px 20px;background:#1a5fff;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer">LOGIN TEST</button>
|
||||
<pre id="out" style="background:#000;padding:16px;margin-top:20px;border-radius:8px;color:#0f0;white-space:pre-wrap"></pre>
|
||||
<script>
|
||||
function log(msg){ document.getElementById('out').textContent += msg + '\n'; }
|
||||
async function go(){
|
||||
document.getElementById('out').textContent = '';
|
||||
log('Klik terdeteksi!');
|
||||
log('URL: ' + window.location.href);
|
||||
const u = document.getElementById('u').value;
|
||||
const p = document.getElementById('p').value;
|
||||
log('Username: ' + u + ', Password: ' + p);
|
||||
log('Mengirim fetch ke: api/auth.php?action=login');
|
||||
try {
|
||||
const res = await fetch('api/auth.php?action=login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({username: u, password: p})
|
||||
});
|
||||
log('HTTP Status: ' + res.status);
|
||||
const raw = await res.text();
|
||||
log('Raw response: [' + raw + ']');
|
||||
try {
|
||||
const json = JSON.parse(raw);
|
||||
log('JSON parse: OK');
|
||||
log('success: ' + json.success);
|
||||
log('message: ' + json.message);
|
||||
if(json.success) log('✅ LOGIN BERHASIL! Role: ' + json.data.role);
|
||||
else log('❌ LOGIN GAGAL: ' + json.message);
|
||||
} catch(e) {
|
||||
log('❌ JSON parse GAGAL: ' + e.message);
|
||||
log('Response tidak valid JSON - ada PHP error di output!');
|
||||
}
|
||||
} catch(e) {
|
||||
log('❌ Fetch error: ' + e.message);
|
||||
log('Kemungkinan: server tidak jalan, CORS, atau network error');
|
||||
}
|
||||
}
|
||||
log('Halaman siap. Tekan tombol LOGIN TEST.');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user