feat: memisahkan monorepo jadi 2 project (Kemiskinan & Publik)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// 1. Fetch all fasilitas_publik
|
||||
$stmtFp = $pdo->query("SELECT * FROM fasilitas_publik ORDER BY id DESC");
|
||||
$fasilitasPublik = $stmtFp->fetchAll();
|
||||
|
||||
// 2. Fetch all penduduk_miskin (dengan nama FP jika ada join)
|
||||
$stmtPm = $pdo->query("
|
||||
SELECT p.*, f.nama as fp_nama
|
||||
FROM penduduk_miskin p
|
||||
LEFT JOIN fasilitas_publik f ON p.fasilitas_publik_id = f.id
|
||||
ORDER BY p.id DESC
|
||||
");
|
||||
$pendudukMiskin = $stmtPm->fetchAll();
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'fasilitasPublik' => $fasilitasPublik,
|
||||
'pendudukMiskin' => $pendudukMiskin
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Database error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id=?");
|
||||
$stmt->execute([$id]);
|
||||
$data = $stmt->fetch();
|
||||
if ($data) {
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} else {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan.']);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm
|
||||
LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id
|
||||
WHERE pm.id=?"
|
||||
);
|
||||
$stmt->execute([$id]);
|
||||
$data = $stmt->fetch();
|
||||
if ($data) {
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} else {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan.']);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak diberikan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM points WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($data) {
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan']);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$totalFP = $pdo->query("SELECT COUNT(*) FROM fasilitas_publik")->fetchColumn();
|
||||
$totalPM = $pdo->query("SELECT COUNT(*) FROM penduduk_miskin")->fetchColumn();
|
||||
$ditangani= $pdo->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_jangkauan='Dalam Jangkauan'")->fetchColumn();
|
||||
$belum = $totalPM - $ditangani;
|
||||
$persen = $totalPM > 0 ? round(($ditangani / $totalPM) * 100, 1) : 0;
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'total_ri' => (int)$totalFP,
|
||||
'total_pm' => (int)$totalPM,
|
||||
'ditangani' => (int)$ditangani,
|
||||
'belum_ditangani'=> (int)$belum,
|
||||
'persentase' => $persen,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? '');
|
||||
|
||||
if (!$id || !$status_bantuan) {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak valid.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET status_bantuan=? WHERE id=?");
|
||||
$stmt->execute([$status_bantuan, $id]);
|
||||
echo json_encode(['status'=>'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
session_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
try {
|
||||
if ($action === 'approve_petugas') {
|
||||
$id = (int)$_POST['id'];
|
||||
$stmt = $pdo->prepare("UPDATE users SET status = 'active' WHERE id = ? AND role = 'petugas'");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'reject_petugas') {
|
||||
$id = (int)$_POST['id'];
|
||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ? AND role = 'petugas' AND status = 'pending'");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'toggle_setting') {
|
||||
$key = $_POST['key'];
|
||||
$val = $_POST['value'];
|
||||
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?");
|
||||
$stmt->execute([$val, $key]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'update_user') {
|
||||
$id = (int)$_POST['id'];
|
||||
$role = $_POST['role'];
|
||||
$status = $_POST['status'];
|
||||
|
||||
// Cek jika ini adalah admin utama
|
||||
$stmt = $pdo->prepare("SELECT username FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$userTarget = $stmt->fetch();
|
||||
if ($userTarget && $userTarget['username'] === 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun Admin Utama tidak dapat diubah hak akses atau statusnya.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek admin terakhir
|
||||
if ($role !== 'admin' || $status !== 'active') {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active' AND id != ?");
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Harus ada minimal satu admin aktif']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE users SET role = ?, status = ? WHERE id = ?");
|
||||
$stmt->execute([$role, $status, $id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
elseif ($action === 'delete_user') {
|
||||
$id = (int)$_POST['id'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT username, role, status FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'User tidak ditemukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user['username'] === 'admin') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun Admin Utama tidak dapat dihapus.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek admin terakhir
|
||||
if ($user['role'] === 'admin' && $user['status'] === 'active') {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE role = 'admin' AND status = 'active' AND id != ?");
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->fetchColumn() == 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Tidak dapat menghapus admin aktif terakhir']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
}
|
||||
else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,630 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$role = $_SESSION['role'] ?? 'guest';
|
||||
$username = $_SESSION['username'] ?? 'User';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom Pink Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background font-sans text-foreground h-screen flex flex-col relative overflow-hidden selection:bg-primary/20 selection:text-primary">
|
||||
|
||||
<!-- Ambient Background Effects -->
|
||||
<div aria-hidden="true" class="pointer-events-none fixed inset-0 z-0">
|
||||
<div class="absolute -top-40 -right-40 h-96 w-96 rounded-full bg-primary/10 blur-[100px]"></div>
|
||||
<div class="absolute top-1/3 -left-40 h-96 w-96 rounded-full bg-rose-500/10 blur-[100px]"></div>
|
||||
<div class="absolute -bottom-40 left-1/2 h-96 w-96 -translate-x-1/2 rounded-full bg-primary/5 blur-[100px]"></div>
|
||||
<!-- Grid pattern -->
|
||||
<div class="absolute inset-0 opacity-[0.03]" style="background-image: linear-gradient(to right, #0f172a 1px, transparent 1px), linear-gradient(to bottom, #0f172a 1px, transparent 1px); background-size: 40px 40px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<header class="relative z-10 w-full border-b border-border/60 bg-white/60 backdrop-blur-xl">
|
||||
<div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6 lg:px-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-rose-700 text-white shadow-md shadow-primary/20">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-heading text-sm font-bold tracking-wide text-foreground">WebGIS Geospasial</h1>
|
||||
<p class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="hidden sm:flex flex-col items-end mr-2">
|
||||
<span class="text-sm font-bold text-foreground capitalize"><?php echo htmlspecialchars($username); ?></span>
|
||||
<span class="text-xs font-semibold text-primary capitalize bg-primary/10 px-2 py-0.5 rounded-full"><?php echo htmlspecialchars($role); ?></span>
|
||||
</div>
|
||||
<div class="h-9 w-9 rounded-full bg-muted border border-border flex items-center justify-center text-muted-foreground font-bold overflow-hidden">
|
||||
<!-- User Avatar Placeholder -->
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
||||
</div>
|
||||
<div class="w-px h-6 bg-border mx-1"></div>
|
||||
<a href="logout.php" class="text-sm font-medium text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5" title="Keluar">
|
||||
<span class="hidden sm:inline">Keluar</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="relative z-10 mx-auto w-full max-w-6xl px-6 py-12 lg:px-8 flex-1 overflow-y-auto">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<div class="mb-12 max-w-3xl">
|
||||
<div class="mb-3 inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-semibold text-primary ring-1 ring-border shadow-sm">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-primary animate-pulse"></span>
|
||||
Sistem Aktif
|
||||
</div>
|
||||
<h2 class="font-heading text-3xl sm:text-4xl font-extrabold tracking-tight text-foreground mb-3">
|
||||
Halo, <span class="text-primary capitalize"><?php echo htmlspecialchars($username); ?></span> 👋
|
||||
</h2>
|
||||
<p class="text-base text-muted-foreground leading-relaxed">
|
||||
Pilih modul analisis spasial yang ingin Anda kerjakan hari ini. Sistem menyediakan berbagai perangkat untuk pemetaan, digitasi, dan analisis data kemiskinan terpadu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- TAHAP 3: Buka Peta Analisis Kemiskinan Banner -->
|
||||
<div class="mb-8 overflow-hidden rounded-3xl border border-rose-200/50 bg-gradient-to-r from-rose-500 via-primary to-rose-700 shadow-xl shadow-primary/20 relative">
|
||||
<div class="absolute inset-0 bg-[url('assets/img/map-bg.png')] opacity-10 bg-cover bg-center mix-blend-overlay"></div>
|
||||
<div class="absolute -right-20 -top-20 h-64 w-64 rounded-full bg-white/10 blur-3xl"></div>
|
||||
<div class="relative z-10 flex flex-col md:flex-row items-center justify-between p-6 md:p-8 gap-6">
|
||||
<div class="text-white text-center md:text-left">
|
||||
<h2 class="text-2xl md:text-3xl font-extrabold font-heading mb-2 flex items-center gap-2 justify-center md:justify-start">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-7 w-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s-8-4-8-10V5l8-3 8 3v7c0 6-8 10-8 10z"></path></svg>
|
||||
Aplikasi Kemiskinan Privat
|
||||
</h2>
|
||||
<p class="text-rose-100 font-medium text-sm md:text-base max-w-xl">
|
||||
Akses eksklusif Admin & Petugas Lapangan. Kelola data spasial penduduk miskin, evaluasi program bantuan, dan update wilayah.
|
||||
</p>
|
||||
</div>
|
||||
<a href="index.php?modul=bantuan" class="flex-shrink-0 group flex items-center justify-center gap-2 rounded-2xl bg-white text-primary px-8 py-4 font-bold shadow-lg transition-all hover:bg-rose-50 hover:scale-105 hover:shadow-xl active:scale-95 text-lg">
|
||||
Buka Peta Analisis Kemiskinan
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 transition-transform group-hover:translate-x-1" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modules Grid -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<!-- Module 1: SPBU -->
|
||||
<a href="index.php?modul=spbu" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-orange-100 text-orange-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 22v-8c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v8"/><path d="M11 22H3"/><path d="M14 9V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v5"/><path d="M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V9.83a2 2 0 0 0-.59-1.42L18 5"/></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Pemetaan SPBU</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Kelola titik lokasi SPBU, pantau status operasional 24 jam, dan update informasi layanan bahan bakar.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 2: Bantuan -->
|
||||
<a href="index.php?modul=bantuan" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Analisis Bantuan</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Data spasial penerima bantuan kemiskinan, upload dokumentasi foto rumah dan KK secara terpadu.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 3: Tematik -->
|
||||
<a href="index.php?modul=tematik" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-blue-100 text-blue-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"></polygon><line x1="9" y1="3" x2="9" y2="18"></line><line x1="15" y1="6" x2="15" y2="21"></line></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Peta Tematik</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Visualisasi sebaran wilayah rawan bencana (Banjir, Longsor, Kebakaran) melalui layer GeoJSON yang interaktif.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 4: Digitasi -->
|
||||
<a href="index.php?modul=digitasi" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-purple-100 text-purple-600 transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">Digitasi Lahan</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Fitur editing geometri spasial. Buat dan edit batas area (polygon) langsung di atas peta untuk pemetaan aset.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Module 5: Utuh -->
|
||||
<a href="index.php?modul=utuh" class="group relative flex flex-col justify-between rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_20px_40px_-15px_rgba(225,29,72,0.15)] hover:border-primary/30 sm:col-span-2 lg:col-span-1">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary transition-transform group-hover:scale-110 group-hover:rotate-3 shadow-inner">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
||||
</div>
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-hover:bg-primary/10 group-hover:text-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-2 group-hover:text-primary transition-colors">WebGIS Terpadu</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Tampilan peta keseluruhan yang menggabungkan semua layer dan modul secara bersamaan.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($role === 'admin'): ?>
|
||||
<?php
|
||||
include 'koneksi.php';
|
||||
// Ambil data petugas pending
|
||||
$stmtPending = $pdo->query("SELECT id, username, email, nama_lengkap, created_at FROM users WHERE role = 'petugas' AND status = 'pending'");
|
||||
$pendingUsers = $stmtPending->fetchAll();
|
||||
|
||||
// Ambil semua data pengguna untuk manajemen
|
||||
$stmtAll = $pdo->query("SELECT id, username, email, nama_lengkap, role, status, created_at FROM users ORDER BY created_at DESC");
|
||||
$allUsers = $stmtAll->fetchAll();
|
||||
|
||||
// Ambil pengaturan show_demo_accounts
|
||||
$stmtSet = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'show_demo_accounts'");
|
||||
$showDemo = '0';
|
||||
if ($r = $stmtSet->fetch()) $showDemo = $r['setting_value'];
|
||||
?>
|
||||
<!-- Admin Dashboard Section -->
|
||||
<div class="mt-12 space-y-8">
|
||||
<h2 class="font-heading text-2xl font-bold text-foreground">Panel Administrator</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Approval Panel -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="font-heading text-lg font-bold text-foreground">Approval Petugas Lapangan</h3>
|
||||
<span class="inline-flex items-center justify-center rounded-full bg-rose-100 px-2.5 py-0.5 text-xs font-bold text-rose-700">
|
||||
<?= count($pendingUsers) ?> Menunggu
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if (count($pendingUsers) > 0): ?>
|
||||
<div class="overflow-x-auto rounded-xl border border-border">
|
||||
<table class="w-full text-left text-sm text-muted-foreground">
|
||||
<thead class="bg-muted/50 text-xs uppercase text-foreground">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Nama Lengkap</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<th class="px-4 py-3 text-right">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
<?php foreach ($pendingUsers as $u): ?>
|
||||
<tr class="hover:bg-muted/30 transition-colors" id="row-<?= $u['id'] ?>">
|
||||
<td class="px-4 py-3 font-medium text-foreground"><?= htmlspecialchars($u['nama_lengkap'] ?: $u['username']) ?></td>
|
||||
<td class="px-4 py-3"><?= htmlspecialchars($u['email'] ?? '-') ?></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<button onclick="handleAdminAction('approve_petugas', <?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-100 text-emerald-600 hover:bg-emerald-200 transition-colors" title="Setujui">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
</button>
|
||||
<button onclick="handleAdminAction('reject_petugas', <?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-rose-100 text-rose-600 hover:bg-rose-200 transition-colors ml-1" title="Tolak">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="flex flex-col items-center justify-center py-8 text-center bg-muted/20 rounded-xl border border-dashed border-border">
|
||||
<span class="text-3xl mb-2">🎉</span>
|
||||
<p class="text-sm font-medium text-foreground">Tidak ada pendaftaran baru</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">Semua petugas telah diverifikasi.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl">
|
||||
<h3 class="font-heading text-lg font-bold text-foreground mb-4">Pengaturan Sistem</h3>
|
||||
|
||||
<div class="flex items-center justify-between rounded-xl border border-border bg-white p-4">
|
||||
<div>
|
||||
<p class="font-semibold text-foreground text-sm">Tampilkan Tombol Akun Demo</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Memunculkan helper login untuk demonstrasi aplikasi.</p>
|
||||
</div>
|
||||
<label class="relative inline-flex cursor-pointer items-center">
|
||||
<input type="checkbox" id="toggleDemo" class="peer sr-only" <?= $showDemo === '1' ? 'checked' : '' ?> onchange="toggleSetting('show_demo_accounts', this.checked ? '1' : '0')">
|
||||
<div class="h-6 w-11 rounded-full bg-slate-200 after:absolute after:top-[2px] after:left-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-primary peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manajemen Pengguna Section -->
|
||||
<div class="rounded-3xl border border-white/60 bg-white/70 p-6 shadow-sm backdrop-blur-xl mt-8">
|
||||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="font-heading text-lg font-bold text-foreground flex items-center gap-2">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-100 text-indigo-600">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
</span>
|
||||
Manajemen Akun Terdaftar
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">Atur hak akses (role) dan status pengguna sistem.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[500px] overflow-y-auto overflow-x-auto rounded-xl border border-border bg-white relative">
|
||||
<table class="w-full text-left text-sm text-muted-foreground whitespace-nowrap">
|
||||
<thead class="bg-muted/50 text-xs uppercase text-foreground sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-semibold">Nama Lengkap</th>
|
||||
<th class="px-4 py-3 font-semibold">Email / Username</th>
|
||||
<th class="px-4 py-3 font-semibold">Role</th>
|
||||
<th class="px-4 py-3 font-semibold">Status</th>
|
||||
<th class="px-4 py-3 font-semibold">Terdaftar</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
<?php foreach ($allUsers as $u): ?>
|
||||
<tr class="hover:bg-muted/30 transition-colors" id="user-row-<?= $u['id'] ?>">
|
||||
<td class="px-4 py-3 font-medium text-foreground">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 rounded-full bg-gradient-to-br from-slate-100 to-slate-200 border border-slate-300 flex items-center justify-center text-slate-600 font-bold">
|
||||
<?= strtoupper(substr($u['nama_lengkap'] ?: $u['username'], 0, 1)) ?>
|
||||
</div>
|
||||
<?= htmlspecialchars($u['nama_lengkap'] ?: '-') ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-foreground"><?= htmlspecialchars($u['email'] ?: '-') ?></span>
|
||||
<span class="text-xs text-muted-foreground">@<?= htmlspecialchars($u['username']) ?></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 relative">
|
||||
<?php if ($u['username'] === 'admin'): ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-full bg-slate-100 px-3 text-xs font-bold tracking-wide text-slate-500 border border-slate-200 shadow-sm cursor-not-allowed">
|
||||
Super Admin
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<div class="custom-select-wrapper relative inline-block min-w-[110px]" data-id="<?= $u['id'] ?>" data-type="role" data-value="<?= $u['role'] ?>">
|
||||
<button type="button" class="select-btn flex items-center justify-between w-full rounded-full border border-slate-200 bg-slate-50/50 py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide text-slate-700 shadow-sm transition-all hover:bg-slate-100 hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
|
||||
<span class="select-text capitalize"><?= $u['role'] ?></span>
|
||||
<svg class="h-3.5 w-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="select-menu hidden absolute z-50 left-0 mt-1.5 w-full rounded-2xl border border-white bg-white/95 backdrop-blur-xl p-1.5 shadow-[0_10px_30px_rgba(0,0,0,0.08)]">
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors mb-1" data-val="admin">Admin</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors mb-1" data-val="petugas">Petugas</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-slate-600 hover:bg-indigo-50 hover:text-indigo-700 transition-colors" data-val="guest">Guest</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 relative">
|
||||
<?php if ($u['username'] === 'admin'): ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-full bg-emerald-50 px-3 text-xs font-bold tracking-wide text-emerald-700 border border-emerald-200 shadow-sm cursor-not-allowed">
|
||||
Active
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<div class="custom-select-wrapper relative inline-block min-w-[110px]" data-id="<?= $u['id'] ?>" data-type="status" data-value="<?= $u['status'] ?>">
|
||||
<button type="button" class="select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 <?= $u['status'] === 'active' ? 'border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 focus:ring-emerald-500/30' : 'border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 focus:ring-amber-500/30' ?>">
|
||||
<span class="select-text capitalize"><?= $u['status'] ?></span>
|
||||
<svg class="h-3.5 w-3.5 <?= $u['status'] === 'active' ? 'text-emerald-500' : 'text-amber-500' ?>" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="select-menu hidden absolute z-50 left-0 mt-1.5 w-full rounded-2xl border border-white bg-white/95 backdrop-blur-xl p-1.5 shadow-[0_10px_30px_rgba(0,0,0,0.08)]">
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-emerald-700 hover:bg-emerald-100 transition-colors mb-1" data-val="active">Active</div>
|
||||
<div class="select-option cursor-pointer rounded-xl px-3 py-2 text-xs font-bold tracking-wide text-amber-700 hover:bg-amber-100 transition-colors" data-val="pending">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<?= date('d M Y, H:i', strtotime($u['created_at'])) ?>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<?php if ($u['id'] != $_SESSION['user_id']): // Jangan tampilkan tombol hapus untuk diri sendiri ?>
|
||||
<button onclick="deleteUser(<?= $u['id'] ?>)" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-rose-50 text-rose-600 hover:bg-rose-100 border border-rose-100 transition-colors" title="Hapus Akun">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="inline-flex h-8 items-center justify-center rounded-lg bg-slate-100 px-2 text-xs font-semibold text-slate-400 border border-slate-200 cursor-not-allowed">Anda</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Admin JS Scripts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
function handleAdminAction(action, id) {
|
||||
Swal.fire({
|
||||
title: action === 'approve_petugas' ? 'Setujui Petugas?' : 'Tolak & Hapus?',
|
||||
text: action === 'approve_petugas' ? 'Akun ini akan langsung aktif.' : 'Akun ini akan dihapus permanen.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: action === 'approve_petugas' ? '#10b981' : '#e11d48',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Lanjutkan!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', action);
|
||||
formData.append('id', id);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success', title: 'Berhasil', showConfirmButton: false, timer: 1500
|
||||
}).then(() => location.reload());
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan sistem.', 'error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSetting(key, val) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'toggle_setting');
|
||||
formData.append('key', key);
|
||||
formData.append('value', val);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true
|
||||
});
|
||||
Toast.fire({ icon: 'success', title: 'Pengaturan disimpan' });
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if(!e.target.closest('.custom-select-wrapper') && !e.target.closest('.select-menu')) {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
}, true);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.custom-select-wrapper').forEach(wrapper => {
|
||||
const btn = wrapper.querySelector('.select-btn');
|
||||
const menu = wrapper.querySelector('.select-menu');
|
||||
const textSpan = wrapper.querySelector('.select-text');
|
||||
const type = wrapper.dataset.type;
|
||||
const id = wrapper.dataset.id;
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Hide other open menus and return them to their wrappers
|
||||
document.querySelectorAll('.select-menu:not(.hidden)').forEach(m => {
|
||||
if(m !== menu) {
|
||||
m.classList.add('hidden');
|
||||
const orig = document.querySelector(`.custom-select-wrapper[data-id="${m.dataset.ownerId}"][data-type="${m.dataset.ownerType}"]`);
|
||||
if(orig) orig.appendChild(m);
|
||||
}
|
||||
});
|
||||
|
||||
if (menu.classList.contains('hidden')) {
|
||||
menu.classList.remove('hidden');
|
||||
|
||||
// Pindahkan ke body agar tidak terpotong oleh overflow tabel atau terjebak backdrop-blur
|
||||
menu.dataset.ownerId = id;
|
||||
menu.dataset.ownerType = type;
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Hitung posisi absolut terhadap dokumen
|
||||
const rect = btn.getBoundingClientRect();
|
||||
menu.style.position = 'absolute';
|
||||
menu.style.top = (rect.bottom + window.scrollY + 4) + 'px';
|
||||
menu.style.left = (rect.left + window.scrollX) + 'px';
|
||||
menu.style.width = rect.width + 'px';
|
||||
menu.style.zIndex = '99999';
|
||||
} else {
|
||||
menu.classList.add('hidden');
|
||||
wrapper.appendChild(menu);
|
||||
}
|
||||
});
|
||||
|
||||
menu.querySelectorAll('.select-option').forEach(opt => {
|
||||
opt.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const val = opt.dataset.val;
|
||||
wrapper.dataset.value = val;
|
||||
textSpan.textContent = val;
|
||||
|
||||
// Kembalikan ke wrapper
|
||||
menu.classList.add('hidden');
|
||||
wrapper.appendChild(menu);
|
||||
|
||||
if(type === 'status') {
|
||||
const icon = btn.querySelector('svg');
|
||||
if(val === 'active') {
|
||||
btn.className = "select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 focus:ring-emerald-500/30";
|
||||
icon.className = "h-3.5 w-3.5 text-emerald-500";
|
||||
} else {
|
||||
btn.className = "select-btn flex items-center justify-between w-full rounded-full border py-1.5 pl-3 pr-2.5 text-xs font-bold tracking-wide shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 focus:ring-amber-500/30";
|
||||
icon.className = "h-3.5 w-3.5 text-amber-500";
|
||||
}
|
||||
}
|
||||
|
||||
updateUserCustom(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function updateUserCustom(id) {
|
||||
const roleWrapper = document.querySelector(`.custom-select-wrapper[data-id="${id}"][data-type="role"]`);
|
||||
const statusWrapper = document.querySelector(`.custom-select-wrapper[data-id="${id}"][data-type="status"]`);
|
||||
|
||||
const role = roleWrapper ? roleWrapper.dataset.value : '';
|
||||
const status = statusWrapper ? statusWrapper.dataset.value : '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'update_user');
|
||||
formData.append('id', id);
|
||||
formData.append('role', role);
|
||||
formData.append('status', status);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true, position: 'top-end', showConfirmButton: false, timer: 2000, timerProgressBar: true
|
||||
});
|
||||
Toast.fire({ icon: 'success', title: 'Akun berhasil diperbarui' });
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error').then(() => location.reload());
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan jaringan.', 'error'));
|
||||
}
|
||||
|
||||
function deleteUser(id) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Akun?',
|
||||
text: 'Akun ini akan dihapus permanen beserta aksesnya.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#e11d48',
|
||||
cancelButtonColor: '#64748b',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete_user');
|
||||
formData.append('id', id);
|
||||
|
||||
fetch('api_admin.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
const row = document.getElementById('user-row-' + id);
|
||||
if (row) row.remove();
|
||||
Swal.fire({
|
||||
icon: 'success', title: 'Dihapus', text: 'Akun berhasil dihapus.', showConfirmButton: false, timer: 1500
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
}).catch(err => Swal.fire('Error', 'Terjadi kesalahan sistem.', 'error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="relative z-10 w-full mt-auto py-8">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center lg:px-8">
|
||||
<p class="text-sm text-muted-foreground font-medium">
|
||||
© <?php echo date('Y'); ?> WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
|
||||
|
||||
try {
|
||||
$pdo->prepare("DELETE FROM fasilitas_publik WHERE id=?")->execute([$id]);
|
||||
echo json_encode(['status'=>'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// fasilitas_publik/simpan.php
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$jenis = trim($_POST['jenis'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$radius = (int)($_POST['radius'] ?? 300);
|
||||
$kontak = trim($_POST['kontak'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
|
||||
if (!$nama || !$jenis || !$latitude || !$longitude) {
|
||||
echo json_encode(['status'=>'error','message'=>'Nama, Jenis, Latitude, dan Longitude wajib diisi.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!is_numeric($latitude) || !is_numeric($longitude) || !is_numeric($radius)) {
|
||||
echo json_encode(['status'=>'error','message'=>'Koordinat dan radius harus berupa angka.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO fasilitas_publik (nama, jenis, alamat, radius, kontak, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$nama, $jenis, $alamat, $radius, $kontak, $latitude, $longitude]);
|
||||
$id = $pdo->lastInsertId();
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id = ?");
|
||||
$stmtSelect->execute([$id]);
|
||||
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$jenis = trim($_POST['jenis'] ?? '');
|
||||
$alamat = trim($_POST['alamat'] ?? '');
|
||||
$radius = (int)($_POST['radius'] ?? 300);
|
||||
$kontak = trim($_POST['kontak'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
|
||||
if (!$id || !$nama || !$jenis || !$latitude || !$longitude) {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak lengkap.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE fasilitas_publik SET nama=?, jenis=?, alamat=?, radius=?, kontak=?, latitude=?, longitude=? WHERE id=?"
|
||||
);
|
||||
$stmt->execute([$nama, $jenis, $alamat, $radius, $kontak, $latitude, $longitude, $id]);
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT * FROM fasilitas_publik WHERE id=?");
|
||||
$stmtSelect->execute([$id]);
|
||||
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// koneksi.php — Database Connection (PDO)
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
||||
define('DB_USER', getenv('DB_USER') ?: 'root');
|
||||
define('DB_PASS', getenv('DB_PASS') !== false ? getenv('DB_PASS') : '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'webgis');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
|
||||
// ============================================================
|
||||
// AUTO-MIGRATION SCRIPT (Sesuai Permintaan)
|
||||
// ============================================================
|
||||
|
||||
// 1. Buat tabel users jika belum ada (fallback)
|
||||
$pdo->exec("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','guest') NOT NULL DEFAULT 'guest',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB");
|
||||
|
||||
// 2. Cek dan tambahkan kolom baru pada tabel users
|
||||
$columns = $pdo->query("SHOW COLUMNS FROM users")->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!in_array('email', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN email VARCHAR(150) UNIQUE NULL AFTER username");
|
||||
}
|
||||
if (!in_array('nama_lengkap', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN nama_lengkap VARCHAR(150) NULL AFTER email");
|
||||
}
|
||||
if (!in_array('status', $columns)) {
|
||||
// Update user lama menjadi active terlebih dahulu jika diperlukan, tapi defaultnya active
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN status ENUM('pending', 'active') NOT NULL DEFAULT 'active' AFTER role");
|
||||
}
|
||||
if (!in_array('auth_provider', $columns)) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN auth_provider ENUM('local', 'google') NOT NULL DEFAULT 'local' AFTER status");
|
||||
}
|
||||
|
||||
// 3. Buat tabel settings
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(50) PRIMARY KEY,
|
||||
setting_value VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB");
|
||||
|
||||
// 4. Masukkan data default settings jika belum ada
|
||||
$pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('show_demo_accounts', '1')");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
die(json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Koneksi database gagal: ' . $e->getMessage()
|
||||
]));
|
||||
}
|
||||
|
||||
// Legacy mysqli compatibility (untuk file yang masih menggunakannya)
|
||||
$conn = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||
if ($conn) {
|
||||
mysqli_set_charset($conn, DB_CHARSET);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Jika sudah login, redirect ke dashboard.php
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
// Ambil pengaturan show_demo_accounts
|
||||
$show_demo = 1; // default
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'show_demo_accounts'");
|
||||
if ($row = $stmt->fetch()) {
|
||||
$show_demo = (int)$row['setting_value'];
|
||||
}
|
||||
} catch (Exception $e) {}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? ''); // Bisa email atau username
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
if ($username && $password) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
|
||||
$stmt->execute([$username, $username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
// Cek status pending
|
||||
if ($user['status'] === 'pending') {
|
||||
$error = 'Akun Anda sedang menunggu persetujuan Admin.';
|
||||
} else {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$error = 'Email/Username atau password salah.';
|
||||
}
|
||||
} else {
|
||||
$error = 'Silakan isi Email/Username dan password.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
accent: '#e11d48',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.group-focus-within\:text-primary:focus-within { color: #e11d48; }
|
||||
.focus-visible\:ring-primary\/20:focus-visible { --tw-ring-color: rgba(225, 29, 72, 0.2); }
|
||||
.hover\:border-primary\/60:hover { border-color: rgba(225, 29, 72, 0.6); }
|
||||
.focus-visible\:border-primary:focus-visible { border-color: #e11d48; }
|
||||
/* Checkbox styling */
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: #e11d48;
|
||||
border-color: #e11d48;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
/* Custom Pink Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Akses Ditolak',
|
||||
text: '<?= htmlspecialchars($error) ?>',
|
||||
confirmButtonColor: '#e11d48'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="relative flex h-screen w-full items-center justify-center overflow-hidden font-sans text-foreground">
|
||||
<!-- Full Screen Background Map -->
|
||||
<img src="assets/img/map-bg.png" alt="Visualisasi peta analisis spasial" class="absolute inset-0 object-cover w-full h-full" onerror="this.src='https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=2074&auto=format&fit=crop'" />
|
||||
<div aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-red-900/90 via-red-950/80 to-black/90"></div>
|
||||
<div aria-hidden="true" class="absolute inset-0 opacity-10" style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 52px 52px;"></div>
|
||||
|
||||
<!-- Giant Card Container -->
|
||||
<div class="relative z-10 flex w-[90%] max-w-5xl flex-col lg:flex-row overflow-hidden rounded-[2rem] border border-white/20 bg-white/10 shadow-2xl backdrop-blur-xl">
|
||||
|
||||
<!-- LEFT SECTION: Public Access -->
|
||||
<div class="flex flex-col justify-between p-8 lg:w-1/2 lg:p-12">
|
||||
<!-- Top / Brand -->
|
||||
<div class="mb-8">
|
||||
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/30">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6 text-white" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl font-bold tracking-tight text-white lg:text-3xl">WebGIS Geospasial</h1>
|
||||
<p class="mt-2 text-sm leading-relaxed text-white/80">
|
||||
Platform pemetaan cerdas untuk analisis spasial persebaran infrastruktur dan demografi kemiskinan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bottom / Features Info -->
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-4 flex items-center gap-2 text-sm font-bold text-white/90 uppercase tracking-wider">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-rose-300" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
</svg>
|
||||
Sistem Pengamanan Tertutup
|
||||
</h2>
|
||||
<div class="flex flex-col gap-4 text-white/80 text-sm">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-0.5 rounded-full bg-white/20 p-1"><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<p><strong>Database Terpusat:</strong> Melacak data penduduk miskin berdasarkan NIK secara real-time.</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-0.5 rounded-full bg-white/20 p-1"><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<p><strong>Tracking Bantuan:</strong> Verifikasi silang penyaluran BLT dan PKH di berbagai wilayah.</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-0.5 rounded-full bg-white/20 p-1"><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<p><strong>Analisis Spasial:</strong> Pemetaan kantong kemiskinan untuk kebijakan tepat sasaran.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SECTION: Login Form -->
|
||||
<div class="flex flex-col justify-center bg-white/95 p-8 lg:w-1/2 lg:p-12">
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="font-heading text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Login Aplikasi Kemiskinan</h2>
|
||||
<p class="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
Silakan masuk untuk mengakses pusat kendali data kemiskinan, memverifikasi bantuan, dan melihat statistik penduduk miskin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form action="login.php" method="POST" class="flex flex-col gap-4">
|
||||
<!-- Input Email / Username -->
|
||||
<div class="group">
|
||||
<label for="username" class="mb-1 block text-xs font-semibold text-foreground group-focus-within:text-primary transition-colors">Email atau Username</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-4.5 w-4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="text" id="username" name="username" class="block w-full rounded-lg border border-border bg-white py-2 pl-9 pr-3 text-sm transition-all focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 hover:border-primary/60" placeholder="admin" required autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Password -->
|
||||
<div class="group">
|
||||
<label for="password" class="mb-1 block text-xs font-semibold text-foreground group-focus-within:text-primary transition-colors">Kata Sandi</label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-4.5 w-4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" class="block w-full rounded-lg border border-border bg-white py-2 pl-9 pr-9 text-sm transition-all focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 hover:border-primary/60" placeholder="••••••••" required autocomplete="current-password">
|
||||
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none">
|
||||
<svg id="eyeIcon" viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer group">
|
||||
<input type="checkbox" name="remember" class="h-3.5 w-3.5 rounded border-border text-primary shadow-sm focus:ring-primary focus:ring-offset-0">
|
||||
<span class="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">Ingat saya</span>
|
||||
</label>
|
||||
<a href="#" class="text-xs font-semibold text-primary hover:text-primary/80">Lupa sandi?</a>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="group mt-2 flex h-10 items-center justify-center gap-2 rounded-lg bg-primary text-sm font-bold text-white shadow-md shadow-primary/25 transition-all hover:-translate-y-0.5 hover:bg-rose-700 hover:shadow-lg active:translate-y-0">
|
||||
Masuk
|
||||
<svg class="h-4 w-4 transition-transform group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center text-xs text-muted-foreground">
|
||||
Belum punya akun? <a href="register.php" class="font-semibold text-primary hover:underline">Daftar sekarang</a>
|
||||
</div>
|
||||
|
||||
<?php if ($show_demo === 1): ?>
|
||||
<!-- Inline Demo Accounts -->
|
||||
<div class="mt-5 flex items-center justify-center gap-2 text-xs">
|
||||
<span class="font-medium text-muted-foreground">Isi Cepat Demo:</span>
|
||||
<button type="button" onclick="fillDemo('admin', 'password')" class="rounded bg-rose-100 px-2 py-1 font-semibold text-rose-700 transition hover:bg-rose-200">👑 Admin</button>
|
||||
<button type="button" onclick="fillDemo('petugas', 'password')" class="rounded bg-slate-100 px-2 py-1 font-semibold text-slate-700 transition hover:bg-slate-200">📝 Petugas</button>
|
||||
<button type="button" onclick="fillDemo('guest', 'password')" class="rounded bg-slate-100 px-2 py-1 font-semibold text-slate-700 transition hover:bg-slate-200">👁️ Guest</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="mt-8 text-center text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
© <script>document.write(new Date().getFullYear())</script> Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Toggle Password Visibility
|
||||
const toggleBtn = document.getElementById('togglePassword');
|
||||
const passInput = document.getElementById('password');
|
||||
const eyeIcon = document.getElementById('eyeIcon');
|
||||
|
||||
if (toggleBtn && passInput) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
if (passInput.type === 'password') {
|
||||
passInput.type = 'text';
|
||||
eyeIcon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
|
||||
} else {
|
||||
passInput.type = 'password';
|
||||
eyeIcon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fillDemo(user, pass) {
|
||||
document.getElementById('username').value = user;
|
||||
document.getElementById('password').value = pass;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params['path'], $params['domain'],
|
||||
$params['secure'], $params['httponly']
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak. Hanya admin yang dapat menghapus data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||
if (!$id) { echo json_encode(['status'=>'error','message'=>'ID tidak valid.']); exit; }
|
||||
|
||||
try {
|
||||
$pdo->prepare("DELETE FROM penduduk_miskin WHERE id=?")->execute([$id]);
|
||||
echo json_encode(['status'=>'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$nik = trim($_POST['nik'] ?? '');
|
||||
$keterangan = trim($_POST['keterangan'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? 'Belum Disalurkan');
|
||||
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
|
||||
|
||||
if (!$nama || !$latitude || !$longitude) {
|
||||
echo json_encode(['status'=>'error','message'=>'Nama dan lokasi wajib diisi.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$valid_status = ['Dalam Jangkauan','Luar Jangkauan'];
|
||||
if (!in_array($status_jangkauan, $valid_status)) $status_jangkauan = 'Luar Jangkauan';
|
||||
|
||||
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
|
||||
|
||||
// Fungsi upload foto
|
||||
function uploadFoto($file_input_name) {
|
||||
if (isset($_FILES[$file_input_name]) && $_FILES[$file_input_name]['error'] === UPLOAD_ERR_OK) {
|
||||
$file_tmp_path = $_FILES[$file_input_name]['tmp_name'];
|
||||
$file_name = $_FILES[$file_input_name]['name'];
|
||||
$file_size = $_FILES[$file_input_name]['size'];
|
||||
$file_type = mime_content_type($file_tmp_path);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
$max_size = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (in_array($file_type, $allowed_mime_types) && $file_size <= $max_size) {
|
||||
$file_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
$new_file_name = uniqid($file_input_name . '_', true) . '.' . $file_extension;
|
||||
$upload_file_dir = '../uploads/';
|
||||
$dest_path = $upload_file_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($file_tmp_path, $dest_path)) {
|
||||
return $new_file_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFoto('foto_rumah');
|
||||
$foto_kk = uploadFoto('foto_kk');
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO penduduk_miskin (nama, nik, keterangan, latitude, longitude, status_jangkauan, fasilitas_publik_id, foto_rumah, foto_kk, status_bantuan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $foto_rumah, $foto_kk, $status_bantuan]);
|
||||
$id = $pdo->lastInsertId();
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
|
||||
$stmtSelect->execute([$id]);
|
||||
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['role']) || !in_array($_SESSION['role'], ['admin', 'petugas'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(403);
|
||||
echo json_encode(['status'=>'error','message'=>'Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../koneksi.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = trim($_POST['nama'] ?? '');
|
||||
$nik = trim($_POST['nik'] ?? '');
|
||||
$keterangan = trim($_POST['keterangan'] ?? '');
|
||||
$latitude = trim($_POST['latitude'] ?? '');
|
||||
$longitude = trim($_POST['longitude'] ?? '');
|
||||
$status_jangkauan= trim($_POST['status_jangkauan']?? 'Luar Jangkauan');
|
||||
$status_bantuan = trim($_POST['status_bantuan'] ?? 'Belum Disalurkan');
|
||||
$fasilitas_publik_id = trim($_POST['fasilitas_publik_id'] ?? '');
|
||||
|
||||
if (!$id || !$nama || !$latitude || !$longitude) {
|
||||
echo json_encode(['status'=>'error','message'=>'Data tidak lengkap.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$fp_id = ($fasilitas_publik_id !== '') ? (int)$fasilitas_publik_id : null;
|
||||
|
||||
// Fungsi upload foto
|
||||
function uploadFoto($file_input_name) {
|
||||
if (isset($_FILES[$file_input_name]) && $_FILES[$file_input_name]['error'] === UPLOAD_ERR_OK) {
|
||||
$file_tmp_path = $_FILES[$file_input_name]['tmp_name'];
|
||||
$file_name = $_FILES[$file_input_name]['name'];
|
||||
$file_size = $_FILES[$file_input_name]['size'];
|
||||
$file_type = mime_content_type($file_tmp_path);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
$max_size = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
if (in_array($file_type, $allowed_mime_types) && $file_size <= $max_size) {
|
||||
$file_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
$new_file_name = uniqid($file_input_name . '_', true) . '.' . $file_extension;
|
||||
$upload_file_dir = '../uploads/';
|
||||
$dest_path = $upload_file_dir . $new_file_name;
|
||||
|
||||
if (move_uploaded_file($file_tmp_path, $dest_path)) {
|
||||
return $new_file_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFoto('foto_rumah');
|
||||
$foto_kk = uploadFoto('foto_kk');
|
||||
|
||||
try {
|
||||
$query = "UPDATE penduduk_miskin SET nama=?, nik=?, keterangan=?, latitude=?, longitude=?, status_jangkauan=?, fasilitas_publik_id=?, status_bantuan=?";
|
||||
$params = [$nama, $nik, $keterangan, $latitude, $longitude, $status_jangkauan, $fp_id, $status_bantuan];
|
||||
|
||||
if ($foto_rumah) {
|
||||
$query .= ", foto_rumah=?";
|
||||
$params[] = $foto_rumah;
|
||||
}
|
||||
if ($foto_kk) {
|
||||
$query .= ", foto_kk=?";
|
||||
$params[] = $foto_kk;
|
||||
}
|
||||
|
||||
$query .= " WHERE id=?";
|
||||
$params[] = $id;
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
|
||||
$stmtSelect = $pdo->prepare("SELECT pm.*, fp.nama AS fp_nama FROM penduduk_miskin pm LEFT JOIN fasilitas_publik fp ON pm.fasilitas_publik_id = fp.id WHERE pm.id = ?");
|
||||
$stmtSelect->execute([$id]);
|
||||
$data = $stmtSelect->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status'=>'error','message'=>$e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
session_start();
|
||||
include '../koneksi.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$latitude = $_POST['latitude'] ?? '';
|
||||
$longitude = $_POST['longitude'] ?? '';
|
||||
$status_jangkauan = $_POST['status_jangkauan'] ?? '';
|
||||
$fasilitas_publik_id = $_POST['fasilitas_publik_id'] ?? null;
|
||||
|
||||
if (!$id || !$latitude || !$longitude) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($fasilitas_publik_id === '') {
|
||||
$fasilitas_publik_id = null;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($status_jangkauan !== '') {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET latitude = ?, longitude = ?, status_jangkauan = ?, fasilitas_publik_id = ? WHERE id = ?");
|
||||
$stmt->execute([$latitude, $longitude, $status_jangkauan, $fasilitas_publik_id, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE penduduk_miskin SET latitude = ?, longitude = ? WHERE id = ?");
|
||||
$stmt->execute([$latitude, $longitude, $id]);
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
session_start();
|
||||
// Jika sudah login, redirect ke dashboard.php
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include 'koneksi.php';
|
||||
|
||||
// Konfigurasi Google OAuth (DUMMY/HARUS DIISI OLEH USER)
|
||||
define('GOOGLE_CLIENT_ID', '575444350307-likfkam3fkppn8rjl8044uppdm4ovf35.apps.googleusercontent.com');
|
||||
define('GOOGLE_CLIENT_SECRET', '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');
|
||||
define('GOOGLE_REDIRECT_URI', 'http://localhost/webgis/register.php');
|
||||
|
||||
$sweetAlertScript = '';
|
||||
|
||||
// Proses Google OAuth Callback
|
||||
if (isset($_GET['code'])) {
|
||||
$code = $_GET['code'];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "https://oauth2.googleapis.com/token");
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||
'client_id' => GOOGLE_CLIENT_ID,
|
||||
'client_secret' => GOOGLE_CLIENT_SECRET,
|
||||
'redirect_uri' => GOOGLE_REDIRECT_URI,
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$token_data = json_decode($response, true);
|
||||
if (isset($token_data['access_token'])) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "https://www.googleapis.com/oauth2/v3/userinfo");
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $token_data['access_token']]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$user_data = json_decode(curl_exec($ch), true);
|
||||
curl_close($ch);
|
||||
|
||||
if (isset($user_data['email'])) {
|
||||
$email = $user_data['email'];
|
||||
$name = $user_data['name'];
|
||||
$state_role = $_GET['state'] ?? 'guest';
|
||||
$role = in_array($state_role, ['guest', 'petugas']) ? $state_role : 'guest';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? OR username = ?");
|
||||
$stmt->execute([$email, $email]);
|
||||
$existing_user = $stmt->fetch();
|
||||
|
||||
if ($existing_user) {
|
||||
if ($existing_user['status'] === 'pending') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'warning', title: 'Menunggu Persetujuan', text: 'Akun Anda sedang menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'});";
|
||||
} else {
|
||||
$_SESSION['user_id'] = $existing_user['id'];
|
||||
$_SESSION['username'] = $existing_user['username'];
|
||||
$_SESSION['role'] = $existing_user['role'];
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$status = ($role === 'petugas') ? 'pending' : 'active';
|
||||
$username = explode('@', $email)[0] . rand(100,999);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, email, nama_lengkap, password, role, status, auth_provider) VALUES (?, ?, ?, ?, ?, ?, 'google')");
|
||||
$stmt->execute([$username, $email, $name, password_hash(bin2hex(random_bytes(8)), PASSWORD_DEFAULT), $role, $status]);
|
||||
|
||||
if ($role === 'petugas') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Pendaftaran via Google berhasil. Akun Anda menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
} else {
|
||||
$_SESSION['user_id'] = $pdo->lastInsertId();
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['role'] = 'guest';
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Google Login Gagal', text: 'Terjadi kesalahan kredensial OAuth.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
|
||||
// Proses Registrasi Lokal
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['register'])) {
|
||||
$nama_lengkap = trim($_POST['nama_lengkap'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
$role = trim($_POST['role'] ?? 'guest');
|
||||
|
||||
if ($nama_lengkap && $email && $password && in_array($role, ['guest', 'petugas'])) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ? OR username = ?");
|
||||
$stmt->execute([$email, $email]);
|
||||
if ($stmt->fetch()) {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Registrasi Gagal', text: 'Email tersebut sudah terdaftar.', confirmButtonColor: '#e11d48'});";
|
||||
} else {
|
||||
$status = ($role === 'petugas') ? 'pending' : 'active';
|
||||
$username = explode('@', $email)[0] . rand(100,999);
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, email, nama_lengkap, password, role, status, auth_provider) VALUES (?, ?, ?, ?, ?, ?, 'local')");
|
||||
if ($stmt->execute([$username, $email, $nama_lengkap, password_hash($password, PASSWORD_DEFAULT), $role, $status])) {
|
||||
if ($role === 'petugas') {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Pendaftaran berhasil. Menunggu persetujuan Admin.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'success', title: 'Pendaftaran Berhasil', text: 'Akun Anda aktif, silakan masuk ke sistem.', confirmButtonColor: '#e11d48'}).then(() => { window.location.href = 'login.php'; });";
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'error', title: 'Gagal', text: 'Terjadi kesalahan sistem internal.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sweetAlertScript = "Swal.fire({icon: 'warning', title: 'Data Belum Lengkap', text: 'Harap lengkapi semua bidang yang tersedia.', confirmButtonColor: '#e11d48'});";
|
||||
}
|
||||
}
|
||||
|
||||
// URL OAuth Google
|
||||
$google_auth_url = "https://accounts.google.com/o/oauth2/v2/auth?client_id=" . GOOGLE_CLIENT_ID . "&redirect_uri=" . urlencode(GOOGLE_REDIRECT_URI) . "&response_type=code&scope=email%20profile";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register – WebGIS Pengentasan Kemiskinan</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
heading: ['"Plus Jakarta Sans"', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
background: '#f8fafc',
|
||||
foreground: '#0f172a',
|
||||
primary: { DEFAULT: '#e11d48', foreground: '#ffffff' },
|
||||
muted: { DEFAULT: '#f1f5f9', foreground: '#64748b' },
|
||||
border: '#e2e8f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.group-focus-within\:text-primary:focus-within { color: #e11d48; }
|
||||
.focus-visible\:ring-primary\/20:focus-visible { --tw-ring-color: rgba(225, 29, 72, 0.2); }
|
||||
.hover\:border-primary\/60:hover { border-color: rgba(225, 29, 72, 0.6); }
|
||||
.focus-visible\:border-primary:focus-visible { border-color: #e11d48; }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #be123c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<?php if ($sweetAlertScript): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<?= $sweetAlertScript ?>
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<main class="flex h-screen w-full overflow-hidden bg-background font-sans text-foreground">
|
||||
<!-- Left: hero / map -->
|
||||
<section class="relative hidden w-1/2 overflow-hidden lg:block">
|
||||
<img src="assets/img/map-bg.png" alt="Visualisasi peta" class="absolute inset-0 object-cover w-full h-full" onerror="this.src='https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=2074&auto=format&fit=crop'" />
|
||||
<div aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-red-900/85 via-red-800/80 to-rose-900/90"></div>
|
||||
<div aria-hidden="true" class="absolute inset-0 opacity-[0.06]" style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 52px 52px;"></div>
|
||||
|
||||
<div class="relative z-10 flex h-full flex-col justify-between p-12 xl:p-16">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-white/15 backdrop-blur-sm ring-1 ring-white/25">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-white" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 21s-7-5.6-7-11a7 7 0 1 1 14 0c0 5.4-7 11-7 11Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-heading text-sm font-bold tracking-wide text-white shadow-sm">WebGIS Geospasial</h1>
|
||||
<p class="text-[11px] font-medium text-white/70 uppercase tracking-wider">Kementerian Analisis Spasial</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-heading text-4xl font-bold leading-tight tracking-tight text-white xl:text-5xl">
|
||||
Bergabung Bersama <br> Komunitas <span class="text-rose-200">Spasial</span>
|
||||
</h2>
|
||||
<p class="mt-4 max-w-md leading-relaxed text-rose-100/90 text-sm xl:text-base">
|
||||
Jadilah bagian dari inisiatif digital pemetaan wilayah, dan ikut serta dalam evaluasi kemiskinan berbasis data geospasial.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right: Register Form -->
|
||||
<section class="flex w-full lg:w-1/2 h-full overflow-y-auto p-6">
|
||||
<div class="w-full max-w-sm sm:max-w-md m-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8 mt-10 lg:mt-0">
|
||||
<h2 class="font-heading text-2xl font-bold tracking-tight text-foreground sm:text-3xl">Buat Akun Baru</h2>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Isi data di bawah ini untuk membuat akun WebGIS Anda.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="rounded-2xl border border-white/60 bg-white/70 p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] backdrop-blur-xl sm:p-8">
|
||||
|
||||
<!-- Google Auth Button -->
|
||||
<button type="button" onclick="startGoogleAuth()" class="flex h-11 w-full items-center justify-center gap-3 rounded-xl border border-border bg-white text-sm font-semibold text-foreground shadow-sm hover:bg-gray-50 transition-colors mb-6">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
Daftar dengan Google
|
||||
</button>
|
||||
|
||||
<div class="relative flex items-center mb-6">
|
||||
<div class="flex-grow border-t border-border"></div>
|
||||
<span class="flex-shrink-0 mx-4 text-xs text-muted-foreground uppercase tracking-wider font-semibold">atau dengan email</span>
|
||||
<div class="flex-grow border-t border-border"></div>
|
||||
</div>
|
||||
|
||||
<form action="register.php" method="POST" class="flex flex-col gap-4">
|
||||
<input type="hidden" name="register" value="1">
|
||||
|
||||
<!-- Input Nama -->
|
||||
<div class="group">
|
||||
<label for="nama_lengkap" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Nama Lengkap
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="nama_lengkap" name="nama_lengkap" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="John Doe" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Email -->
|
||||
<div class="group">
|
||||
<label for="email" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Alamat Email
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="email" id="email" name="email" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="john@example.com" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Password -->
|
||||
<div class="group">
|
||||
<label for="password" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Kata Sandi
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="password" id="password" name="password" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 pr-10 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60" placeholder="••••••••" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Role -->
|
||||
<div class="group">
|
||||
<label for="role" class="mb-1.5 block text-sm font-semibold text-foreground group-focus-within:text-primary transition-colors">
|
||||
Daftar Sebagai
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select id="role" name="role" class="block w-full rounded-xl border border-border bg-white py-2.5 px-3 text-sm text-foreground transition-all focus:outline-none focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 hover:border-primary/60 appearance-none" required>
|
||||
<option value="guest">Guest (Pengunjung Umum)</option>
|
||||
<option value="petugas">Petugas (Verifikator Lapangan)</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-muted-foreground">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="group mt-3 flex h-12 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-semibold text-white shadow-lg shadow-primary/25 transition-all duration-300 hover:-translate-y-0.5 hover:bg-rose-700 hover:shadow-xl hover:shadow-primary/35 active:translate-y-0">
|
||||
Daftar Sekarang
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 text-center text-sm text-muted-foreground">
|
||||
Sudah punya akun? <a href="login.php" class="font-semibold text-primary hover:underline">Masuk di sini</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-xs text-muted-foreground">
|
||||
© <script>document.write(new Date().getFullYear())</script> WebGIS Geospasial · Kementerian Analisis Spasial
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function startGoogleAuth() {
|
||||
const role = document.getElementById('role').value;
|
||||
window.location.href = "<?= $google_auth_url ?>" + "&state=" + encodeURIComponent(role);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
# Gitkeep
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 366 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 374 KiB |
Reference in New Issue
Block a user