diff --git a/03/admin_reset_password.php b/03/admin_reset_password.php
new file mode 100644
index 0000000..0dcde10
--- /dev/null
+++ b/03/admin_reset_password.php
@@ -0,0 +1,46 @@
+ false, 'message' => 'Akses ditolak']);
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ echo json_encode(['success' => false, 'message' => 'Hanya POST']);
+ exit;
+}
+
+$request_id = (int)($_POST['request_id'] ?? 0);
+$user_id = (int)($_POST['user_id'] ?? 0);
+$new_password = $_POST['new_password'] ?? '';
+
+if ($request_id <= 0 || $user_id <= 0 || strlen($new_password) < 6) {
+ echo json_encode(['success' => false, 'message' => 'Password minimal 6 karakter']);
+ exit;
+}
+
+// Update password user
+$hashed = password_hash($new_password, PASSWORD_DEFAULT);
+$stmt = $conn->prepare("UPDATE users SET password = ? WHERE id = ?");
+$stmt->bind_param("si", $hashed, $user_id);
+if (!$stmt->execute()) {
+ echo json_encode(['success' => false, 'message' => 'Gagal update password']);
+ $stmt->close(); $conn->close(); exit;
+}
+$stmt->close();
+
+// Tandai request sebagai done
+$stmt2 = $conn->prepare("UPDATE password_resets SET status = 'done' WHERE id = ?");
+$stmt2->bind_param("i", $request_id);
+$stmt2->execute();
+$stmt2->close();
+
+echo json_encode(['success' => true, 'message' => 'Password berhasil direset. User bisa login dengan password baru.']);
+$conn->close();
diff --git a/03/auth_check.php b/03/auth_check.php
new file mode 100644
index 0000000..448c5a3
--- /dev/null
+++ b/03/auth_check.php
@@ -0,0 +1,20 @@
+ true,
+ 'user' => [
+ 'id' => $_SESSION['user_id'],
+ 'nama' => $_SESSION['user_nama'],
+ 'email' => $_SESSION['user_email'],
+ 'role' => $_SESSION['user_role']
+ ]
+ ]);
+} else {
+ echo json_encode(['logged_in' => false]);
+}
diff --git a/03/auth_login.php b/03/auth_login.php
new file mode 100644
index 0000000..9086b40
--- /dev/null
+++ b/03/auth_login.php
@@ -0,0 +1,76 @@
+ false, 'message' => 'Hanya POST']);
+ exit;
+}
+
+$email = trim($_POST['email'] ?? '');
+$password = $_POST['password'] ?? '';
+
+if (empty($email) || empty($password)) {
+ echo json_encode(['success' => false, 'message' => 'Email dan password wajib diisi']);
+ exit;
+}
+
+$stmt = $conn->prepare("SELECT id, nama, email, password, role, status FROM users WHERE email = ?");
+$stmt->bind_param("s", $email);
+$stmt->execute();
+$result = $stmt->get_result();
+
+if ($result->num_rows === 0) {
+ echo json_encode(['success' => false, 'message' => 'Email tidak ditemukan']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+
+$user = $result->fetch_assoc();
+
+if (!password_verify($password, $user['password'])) {
+ echo json_encode(['success' => false, 'message' => 'Password salah']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+
+// Cek status verifikasi akun
+if ($user['status'] === 'pending') {
+ echo json_encode(['success' => false, 'message' => 'Akun Anda sedang menunggu verifikasi dari Admin. Harap bersabar.']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+if ($user['status'] === 'rejected') {
+ echo json_encode(['success' => false, 'message' => 'Akun Anda ditolak oleh Admin.']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+
+// Set session
+$_SESSION['user_id'] = (int)$user['id'];
+$_SESSION['user_nama'] = $user['nama'];
+$_SESSION['user_email']= $user['email'];
+$_SESSION['user_role'] = $user['role'];
+
+echo json_encode([
+ 'success' => true,
+ 'message' => 'Login berhasil!',
+ 'user' => [
+ 'id' => (int)$user['id'],
+ 'nama' => $user['nama'],
+ 'email' => $user['email'],
+ 'role' => $user['role']
+ ]
+]);
+
+$stmt->close();
+$conn->close();
diff --git a/03/auth_logout.php b/03/auth_logout.php
new file mode 100644
index 0000000..e60941c
--- /dev/null
+++ b/03/auth_logout.php
@@ -0,0 +1,18 @@
+ true]);
+} else {
+ // If navigated directly, redirect to login
+ header('Location: login.html');
+ exit;
+}
diff --git a/03/auth_register.php b/03/auth_register.php
new file mode 100644
index 0000000..e04aff5
--- /dev/null
+++ b/03/auth_register.php
@@ -0,0 +1,69 @@
+ false, 'message' => 'Hanya POST']);
+ exit;
+}
+
+$nama = trim($_POST['nama'] ?? '');
+$email = trim($_POST['email'] ?? '');
+$password = $_POST['password'] ?? '';
+$role = $_POST['role'] ?? 'viewer';
+
+// Validasi
+if (empty($nama)) {
+ echo json_encode(['success' => false, 'message' => 'Nama wajib diisi']);
+ exit;
+}
+if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ echo json_encode(['success' => false, 'message' => 'Email tidak valid']);
+ exit;
+}
+if (strlen($password) < 6) {
+ echo json_encode(['success' => false, 'message' => 'Password minimal 6 karakter']);
+ exit;
+}
+// Validasi role (admin tidak boleh daftar sendiri)
+if (!in_array($role, ['surveyer', 'viewer'])) {
+ echo json_encode(['success' => false, 'message' => 'Pendaftaran untuk role tersebut tidak diizinkan']);
+ exit;
+}
+
+// Cek email sudah terdaftar
+$stmt = $conn->prepare("SELECT id FROM users WHERE email = ?");
+$stmt->bind_param("s", $email);
+$stmt->execute();
+if ($stmt->get_result()->num_rows > 0) {
+ echo json_encode(['success' => false, 'message' => 'Email sudah terdaftar']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+$stmt->close();
+
+// Hash password & simpan
+$hashed = password_hash($password, PASSWORD_DEFAULT);
+$stmt = $conn->prepare("INSERT INTO users (nama, email, password, role) VALUES (?, ?, ?, ?)");
+$stmt->bind_param("ssss", $nama, $email, $hashed, $role);
+
+if ($stmt->execute()) {
+ $userId = $conn->insert_id;
+
+ // Jangan auto-login. Menunggu verifikasi admin.
+ echo json_encode([
+ 'success' => true,
+ 'message' => 'Registrasi berhasil! Akun Anda sedang menunggu verifikasi dari Admin.'
+ ]);
+} else {
+ echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $conn->error]);
+}
+
+$stmt->close();
+$conn->close();
diff --git a/03/get_laporan_user.php b/03/get_laporan_user.php
new file mode 100644
index 0000000..2f24a46
--- /dev/null
+++ b/03/get_laporan_user.php
@@ -0,0 +1,32 @@
+ false, 'message' => 'Belum login']);
+ exit;
+}
+
+$response = ['success' => true, 'reports' => []];
+
+$result = $conn->query("SELECT * FROM laporan ORDER BY created_at DESC LIMIT 100");
+if ($result) {
+ while ($row = $result->fetch_assoc()) {
+ $response['reports'][] = [
+ 'id' => (int)$row['id'],
+ 'name' => $row['pelapor'] ?? 'Anonim',
+ 'text' => $row['deskripsi'],
+ 'imgBase64'=> $row['foto_base64'] ?? null,
+ 'time' => date('d/m/Y H:i', strtotime($row['created_at']))
+ ];
+ }
+ $result->free();
+}
+
+echo json_encode($response);
+$conn->close();
diff --git a/03/get_pending_users.php b/03/get_pending_users.php
new file mode 100644
index 0000000..8b83372
--- /dev/null
+++ b/03/get_pending_users.php
@@ -0,0 +1,26 @@
+ false, 'message' => 'Akses ditolak. Hanya Admin.']);
+ exit;
+}
+
+$stmt = $conn->prepare("SELECT id, nama, email, role, DATE_FORMAT(created_at, '%d %M %Y %H:%i') as waktu FROM users WHERE status = 'pending' ORDER BY created_at ASC");
+$stmt->execute();
+$result = $stmt->get_result();
+
+$users = [];
+while ($row = $result->fetch_assoc()) {
+ $users[] = $row;
+}
+
+echo json_encode(['success' => true, 'data' => $users]);
+$stmt->close();
+$conn->close();
diff --git a/03/get_reset_requests.php b/03/get_reset_requests.php
new file mode 100644
index 0000000..30b3cd1
--- /dev/null
+++ b/03/get_reset_requests.php
@@ -0,0 +1,32 @@
+ false, 'message' => 'Akses ditolak']);
+ exit;
+}
+
+$stmt = $conn->prepare("
+ SELECT pr.id, pr.user_id, pr.email, u.nama, u.role,
+ DATE_FORMAT(pr.created_at, '%d %M %Y %H:%i') as waktu
+ FROM password_resets pr
+ JOIN users u ON u.id = pr.user_id
+ WHERE pr.status = 'pending'
+ ORDER BY pr.created_at ASC
+");
+$stmt->execute();
+$result = $stmt->get_result();
+
+$requests = [];
+while ($row = $result->fetch_assoc()) {
+ $requests[] = $row;
+}
+
+echo json_encode(['success' => true, 'data' => $requests]);
+$stmt->close();
+$conn->close();
diff --git a/03/index.html b/03/index.php
similarity index 77%
rename from 03/index.html
rename to 03/index.php
index c7261f8..6947341 100644
--- a/03/index.html
+++ b/03/index.php
@@ -1,3 +1,11 @@
+
@@ -35,67 +43,36 @@
Pelaporan
0
+
+
-
+
- 👑
- Admin
+ '👑','surveyer'=>'📋','viewer'=>'👁'];
+ echo $roleIcons[$_SESSION['user_role'] ?? ''] ?? '👤';
+ ?>
+ 'Admin','surveyer'=>'Surveyer','viewer'=>'Masyarakat'];
+ echo htmlspecialchars($roleLabels[$_SESSION['user_role'] ?? ''] ?? 'Guest');
+ ?>
-
+
+
+
+
🚪 Keluar
-
-
-
-
-
-
Pilih role sesuai hak akses Anda. Perubahan langsung berlaku.
-
-
-
👑
-
Admin
-
Akses penuh — tambah, edit, hapus, lihat semua data dan laporan
-
- ✓ Tambah & Edit
- ✓ Hapus & Reset
- ✓ Lihat laporan
- ✓ Kirim laporan
-
-
-
-
📋
-
Surveyer
-
Input data & kelola laporan — tidak bisa hapus atau reset
-
- ✓ Tambah & Edit
- ✕ Hapus & Reset
- ✓ Lihat laporan
- ✓ Kirim laporan
-
-
-
-
👁
-
Viewer
-
Hanya lihat peta & kirim laporan masyarakat
-
- ✓ Lihat peta
- ✕ Edit data
- ✕ Lihat laporan
- ✓ Kirim laporan
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/03/koneksi.php b/03/koneksi.php
index b786c1b..2fe16b8 100644
--- a/03/koneksi.php
+++ b/03/koneksi.php
@@ -1,11 +1,12 @@
connect_error) {
+ $conn = new mysqli('localhost', 'root', '', 'webgis_bansos');
+}
if ($conn->connect_error) {
http_response_code(500);
diff --git a/03/login.html b/03/login.html
new file mode 100644
index 0000000..58475b5
--- /dev/null
+++ b/03/login.html
@@ -0,0 +1,1007 @@
+
+
+
+
+
+ Login — BantSOSial GIS Pontianak
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/03/request_reset.php b/03/request_reset.php
new file mode 100644
index 0000000..6d337c6
--- /dev/null
+++ b/03/request_reset.php
@@ -0,0 +1,62 @@
+ false, 'message' => 'Hanya POST']);
+ exit;
+}
+
+$email = trim($_POST['email'] ?? '');
+
+if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ echo json_encode(['success' => false, 'message' => 'Email tidak valid']);
+ exit;
+}
+
+// Cek apakah email terdaftar
+$stmt = $conn->prepare("SELECT id, nama FROM users WHERE email = ? AND status = 'approved'");
+$stmt->bind_param("s", $email);
+$stmt->execute();
+$result = $stmt->get_result();
+
+if ($result->num_rows === 0) {
+ // Jangan reveal apakah email terdaftar atau tidak (keamanan)
+ echo json_encode(['success' => true, 'message' => 'Permintaan reset password Anda telah dikirim ke Admin.']);
+ $stmt->close();
+ $conn->close();
+ exit;
+}
+
+$user = $result->fetch_assoc();
+$stmt->close();
+
+// Cek apakah sudah ada request pending
+$stmt2 = $conn->prepare("SELECT id FROM password_resets WHERE user_id = ? AND status = 'pending'");
+$stmt2->bind_param("i", $user['id']);
+$stmt2->execute();
+if ($stmt2->get_result()->num_rows > 0) {
+ echo json_encode(['success' => true, 'message' => 'Permintaan reset sudah dikirim sebelumnya. Harap tunggu Admin memproses.']);
+ $stmt2->close();
+ $conn->close();
+ exit;
+}
+$stmt2->close();
+
+// Simpan request reset
+$stmt3 = $conn->prepare("INSERT INTO password_resets (user_id, email) VALUES (?, ?)");
+$stmt3->bind_param("is", $user['id'], $email);
+
+if ($stmt3->execute()) {
+ echo json_encode(['success' => true, 'message' => 'Permintaan reset password telah dikirim ke Admin. Harap tunggu konfirmasi.']);
+} else {
+ echo json_encode(['success' => false, 'message' => 'Gagal mengirim permintaan: ' . $conn->error]);
+}
+
+$stmt3->close();
+$conn->close();
diff --git a/03/script.js b/03/script.js
index 07c00f0..44b2b60 100644
--- a/03/script.js
+++ b/03/script.js
@@ -34,22 +34,76 @@ const ROLE_CONFIG = {
canReport: true, canViewReport: true, canViewDetail: true, canEditKas: false, canDragRadius: false
},
viewer: {
- label: 'Viewer', icon: '👁',
+ label: 'Masyarakat', icon: '👁',
canAdd: false, canEdit: false, canDelete: false, canReset: false,
canReport: true, canViewReport: false, canViewDetail: true, canEditKas: false, canDragRadius: false
}
};
+// User session data
+let loggedInUser = null;
+
+// Auth check — redirect to login if not logged in
+async function checkAuth() {
+ try {
+ const res = await fetch('auth_check.php');
+ const data = await res.json();
+ if (data.logged_in) {
+ loggedInUser = data.user;
+ currentRole = data.user.role;
+ updateUserUI();
+ return true;
+ } else {
+ window.location.href = 'login.html';
+ return false;
+ }
+ } catch (e) {
+ // Server not available — allow offline usage with default role
+ console.log('Auth server tidak tersedia, mode lokal.');
+ return true;
+ }
+}
+
+function updateUserUI() {
+ if (!loggedInUser) return;
+ const cfg = ROLE_CONFIG[loggedInUser.role] || ROLE_CONFIG.viewer;
+
+ // Update role icon (label already set by PHP)
+ const roleIconEl = document.getElementById('roleIcon');
+ if (roleIconEl) roleIconEl.textContent = cfg.icon;
+
+ // Show history nav for viewer (masyarakat)
+ const navHistory = document.getElementById('navHistory');
+ if (navHistory) {
+ navHistory.style.display = loggedInUser.role === 'viewer' ? 'flex' : 'none';
+ }
+
+ // Show verify nav for admin
+ const navVerify = document.getElementById('navVerify');
+ if (navVerify) {
+ navVerify.style.display = loggedInUser.role === 'admin' ? 'flex' : 'none';
+ }
+
+ // Hide add buttons for viewer
+ applyRoleUI();
+
+ // Auto-fill report name if logged in
+ const reportNameEl = document.getElementById('reportName');
+ if (reportNameEl && loggedInUser.nama) {
+ reportNameEl.value = loggedInUser.nama;
+ reportNameEl.readOnly = true;
+ reportNameEl.style.background = '#f0f4f8';
+ }
+}
+
+
function openRoleModal() {
- document.getElementById('roleModal').classList.remove('hidden');
- // Highlight active role card
- document.querySelectorAll('.role-card').forEach(c => c.classList.remove('active-role'));
- const activeCard = document.getElementById(`roleCard_${currentRole}`);
- if (activeCard) activeCard.classList.add('active-role');
+ // Role modal removed — role is from database/session
+ // No-op for backward compatibility
}
function closeRoleModal() {
- document.getElementById('roleModal').classList.add('hidden');
+ // No-op
}
function setRole(role) {
@@ -60,13 +114,8 @@ function setRole(role) {
document.getElementById('roleIcon').textContent = cfg.icon;
document.getElementById('roleLabel').textContent = cfg.label;
- // Role card highlight
- document.querySelectorAll('.role-card').forEach(c => c.classList.remove('active-role'));
- document.getElementById(`roleCard_${role}`)?.classList.add('active-role');
-
// Apply UI restrictions
applyRoleUI();
- closeRoleModal();
}
function applyRoleUI() {
@@ -147,24 +196,28 @@ function applyRoleUI() {
function navigateTo(page) {
const pagePeta = document.getElementById('pagePeta');
const pagePelaporan = document.getElementById('pagePelaporan');
+ const pageHistory = document.getElementById('pageHistory');
const navHome = document.getElementById('navHome');
const navReport = document.getElementById('navReport');
+ const navHistory = document.getElementById('navHistory');
+
+ // Hide all pages
+ [pagePeta, pagePelaporan, pageHistory].forEach(p => { if(p) p.classList.remove('active-page'); });
+ [navHome, navReport, navHistory].forEach(n => { if(n) n.classList.remove('active'); });
if (page === 'map') {
pagePeta.classList.add('active-page');
- pagePelaporan.classList.remove('active-page');
navHome.classList.add('active');
- navReport.classList.remove('active');
- // Fix map rendering when returning to map page
setTimeout(() => map.invalidateSize(), 100);
} else if (page === 'report') {
- pagePeta.classList.remove('active-page');
pagePelaporan.classList.add('active-page');
- navHome.classList.remove('active');
navReport.classList.add('active');
- // Apply role UI for report page columns
applyRoleUI();
renderReportList();
+ } else if (page === 'history') {
+ pageHistory.classList.add('active-page');
+ if (navHistory) navHistory.classList.add('active');
+ loadHistory();
}
}
@@ -1625,10 +1678,307 @@ function highlightError(id, msg) {
el.style.borderColor = '#dc2626'; el.placeholder = msg; el.focus();
}
+// ============================================================
+// VERIFIKASI AKUN (Admin)
+// ============================================================
+function openVerifyModal() {
+ document.getElementById('verifyModal').classList.remove('hidden');
+ switchAdminTab('verify');
+ loadPendingUsers();
+ loadResetRequests();
+}
+
+function closeVerifyModal() {
+ document.getElementById('verifyModal').classList.add('hidden');
+}
+
+function switchAdminTab(tab) {
+ const panelVerify = document.getElementById('adminPanelVerify');
+ const panelReset = document.getElementById('adminPanelReset');
+ const tabVerify = document.getElementById('vTabVerify');
+ const tabReset = document.getElementById('vTabReset');
+
+ if (tab === 'verify') {
+ panelVerify.style.display = 'block';
+ panelReset.style.display = 'none';
+ tabVerify.style.color = 'var(--primary)';
+ tabVerify.style.borderBottom = '3px solid var(--primary)';
+ tabVerify.style.fontWeight = '700';
+ tabReset.style.color = 'var(--text-muted)';
+ tabReset.style.borderBottom = '3px solid transparent';
+ tabReset.style.fontWeight = '600';
+ } else {
+ panelVerify.style.display = 'none';
+ panelReset.style.display = 'block';
+ tabReset.style.color = 'var(--primary)';
+ tabReset.style.borderBottom = '3px solid var(--primary)';
+ tabReset.style.fontWeight = '700';
+ tabVerify.style.color = 'var(--text-muted)';
+ tabVerify.style.borderBottom = '3px solid transparent';
+ tabVerify.style.fontWeight = '600';
+ }
+}
+
+async function loadPendingUsers() {
+ const container = document.getElementById('verifyListContainer');
+ container.innerHTML = 'Memuat data...
';
+ try {
+ const res = await fetch('get_pending_users.php');
+ const data = await res.json();
+ if (data.success) {
+ if (data.data.length === 0) {
+ container.innerHTML = '✅ Tidak ada akun yang menunggu verifikasi.
';
+ } else {
+ let html = '';
+ data.data.forEach(u => {
+ const roleLabel = ROLE_CONFIG[u.role] ? ROLE_CONFIG[u.role].label : u.role;
+ html += `
+
+
+
${u.nama}
+
${u.email} • ${roleLabel}
+
🕒 Terdaftar: ${u.waktu}
+
+
+
+
+
+
+ `;
+ });
+ html += '
';
+ container.innerHTML = html;
+ }
+ } else {
+ container.innerHTML = `Gagal memuat: ${data.message}
`;
+ }
+ } catch (e) {
+ container.innerHTML = `Error mengambil data.
`;
+ }
+}
+
+async function verifyUserAction(userId, action) {
+ const actionText = action === 'approve' ? 'menyetujui' : 'menolak';
+ if (!confirm(`Yakin ingin ${actionText} akun ini?`)) return;
+
+ try {
+ const res = await fetch('verify_user.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: `user_id=${userId}&action=${action}`
+ });
+ const data = await res.json();
+ if (data.success) {
+ alert(data.message);
+ loadPendingUsers();
+ } else {
+ alert('Gagal: ' + data.message);
+ }
+ } catch (e) {
+ alert('Terjadi kesalahan koneksi.');
+ }
+}
+
+async function loadResetRequests() {
+ const container = document.getElementById('resetListContainer');
+ if (!container) return;
+ container.innerHTML = 'Memuat data...
';
+ try {
+ const res = await fetch('get_reset_requests.php');
+ const data = await res.json();
+ if (data.success) {
+ // Update badge
+ const badge = document.getElementById('resetBadge');
+ if (badge) {
+ if (data.data.length > 0) {
+ badge.style.display = 'inline';
+ badge.textContent = data.data.length;
+ } else {
+ badge.style.display = 'none';
+ }
+ }
+
+ if (data.data.length === 0) {
+ container.innerHTML = '✅ Tidak ada permintaan reset password.
';
+ } else {
+ let html = '';
+ data.data.forEach(r => {
+ const roleLabel = ROLE_CONFIG[r.role] ? ROLE_CONFIG[r.role].label : r.role;
+ html += `
+
+
+
+
🔑 ${r.nama}
+
${r.email} • ${roleLabel}
+
🕒 Diminta: ${r.waktu}
+
+
+
+
+
+
+
+
+
+
+ `;
+ });
+ html += '
';
+ container.innerHTML = html;
+ }
+ } else {
+ container.innerHTML = `Gagal: ${data.message}
`;
+ }
+ } catch (e) {
+ container.innerHTML = `Error mengambil data.
`;
+ }
+}
+
+function toggleAdminResetPassword(id) {
+ const input = document.getElementById(`newPw_${id}`);
+ const btn = document.getElementById(`togglePw_${id}`);
+ if (input.type === 'password') {
+ input.type = 'text';
+ btn.textContent = '🔒';
+ } else {
+ input.type = 'password';
+ btn.textContent = '👁';
+ }
+}
+
+async function adminDoReset(requestId, userId) {
+ const newPw = document.getElementById(`newPw_${requestId}`)?.value || '';
+ if (newPw.length < 6) {
+ alert('Password baru minimal 6 karakter!');
+ return;
+ }
+ if (!confirm(`Yakin ingin mereset password untuk user ini?`)) return;
+
+ try {
+ const res = await fetch('admin_reset_password.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: `request_id=${requestId}&user_id=${userId}&new_password=${encodeURIComponent(newPw)}`
+ });
+ const data = await res.json();
+ if (data.success) {
+ alert('✅ ' + data.message);
+ loadResetRequests();
+ } else {
+ alert('❌ Gagal: ' + data.message);
+ }
+ } catch (e) {
+ alert('Terjadi kesalahan koneksi.');
+ }
+}
+
+
+// ============================================================
+// LOGOUT
+// ============================================================
+function doLogout() {
+ if (!confirm('Yakin ingin keluar?')) return;
+ // Redirect langsung ke server — session pasti ter-destroy
+ window.location.href = 'auth_logout.php';
+}
+
+
+// ============================================================
+// HISTORY PELAPORAN (Masyarakat — read-only)
+// ============================================================
+let historyReports = [];
+
+async function loadHistory() {
+ try {
+ const res = await fetch('get_laporan_user.php');
+ const data = await res.json();
+ if (data.success && data.reports) {
+ historyReports = data.reports;
+ renderHistory();
+ }
+ } catch(e) {
+ // If backend not available, show reports from memory
+ historyReports = [...reports];
+ renderHistory();
+ }
+}
+
+function renderHistory() {
+ const container = document.getElementById('historyList');
+ if (!container) return;
+
+ // Update stats
+ const total = historyReports.length;
+ const baru = historyReports.filter(r => (r.status || 'baru') === 'baru').length;
+ const ditangani = historyReports.filter(r => r.status === 'ditangani').length;
+ const selesai = historyReports.filter(r => r.status === 'selesai').length;
+
+ const elTotal = document.getElementById('historyTotal');
+ const elBaru = document.getElementById('historyBaru');
+ const elDitangani = document.getElementById('historyDitangani');
+ const elSelesai = document.getElementById('historySelesai');
+ if (elTotal) elTotal.textContent = total;
+ if (elBaru) elBaru.textContent = baru;
+ if (elDitangani) elDitangani.textContent = ditangani;
+ if (elSelesai) elSelesai.textContent = selesai;
+
+ filterHistory();
+}
+
+function filterHistory() {
+ const q = (document.getElementById('searchHistory')?.value || '').toLowerCase();
+ let filtered = [...historyReports];
+ if (q) {
+ filtered = filtered.filter(r =>
+ (r.name || '').toLowerCase().includes(q) ||
+ (r.text || '').toLowerCase().includes(q)
+ );
+ }
+ renderHistoryCards(filtered);
+}
+
+function renderHistoryCards(list) {
+ const container = document.getElementById('historyList');
+ if (!container) return;
+
+ if (!list || list.length === 0) {
+ container.innerHTML = '📭 Tidak ada laporan yang ditemukan.
';
+ return;
+ }
+
+ const statusLabels = {
+ baru: '🆕 Baru',
+ ditangani: '🔄 Ditangani',
+ selesai: '✅ Selesai'
+ };
+
+ container.innerHTML = list.map(r => {
+ const statusClass = r.status || 'baru';
+ return `
+
+
+
👤 ${r.name || 'Anonim'}
+
🕐 ${r.time || ''}
+
+
${r.text || ''}
+ ${r.imgBase64 ? `

` : ''}
+
+
`;
+ }).join('');
+}
+
// ============================================================
// INIT
// ============================================================
-(function init() {
+(async function init() {
+ const isAuth = await checkAuth();
+ if (!isAuth) return; // Will redirect to login
+
+ // Set role from session
+ setRole(currentRole);
loadFromBackend();
updateStats();
applyRoleUI();
diff --git a/03/setup_users.php b/03/setup_users.php
new file mode 100644
index 0000000..f73bab5
--- /dev/null
+++ b/03/setup_users.php
@@ -0,0 +1,22 @@
+query($sql)) {
+ echo json_encode(['success' => true, 'message' => 'Tabel users berhasil dibuat!']);
+} else {
+ echo json_encode(['success' => false, 'message' => 'Gagal: ' . $conn->error]);
+}
+
+$conn->close();
diff --git a/03/simpan_laporan.php b/03/simpan_laporan.php
index 12e651e..c9cd126 100644
--- a/03/simpan_laporan.php
+++ b/03/simpan_laporan.php
@@ -3,13 +3,22 @@
* simpan_laporan.php — Simpan Laporan Masyarakat
* POST: name, text, img (base64, opsional)
*/
+session_start();
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'message'=>'Hanya POST']); exit;
}
-$name = $conn->real_escape_string(strip_tags($_POST['name'] ?? 'Anonim'));
+// Ambil nama dari session jika ada, fallback ke POST
+$name = '';
+if (isset($_SESSION['user_nama']) && !empty($_SESSION['user_nama'])) {
+ $name = $_SESSION['user_nama'];
+} else {
+ $name = strip_tags($_POST['name'] ?? 'Anonim');
+}
+$name = $conn->real_escape_string($name);
+
$text = $conn->real_escape_string(strip_tags($_POST['text'] ?? ''));
$img = $_POST['img'] ?? '';
diff --git a/03/style.css b/03/style.css
index d6e81ff..d1f3c70 100644
--- a/03/style.css
+++ b/03/style.css
@@ -95,6 +95,22 @@ html, body { height: 100%; font-family: var(--font); background: var(--bg); over
font-size: 12px; font-weight: 600;
}
+.user-name-pill {
+ display: flex; align-items: center; gap: 6px;
+ background: white; border: 1.5px solid var(--border);
+ border-radius: 20px; padding: 5px 12px;
+ font-size: 12px; font-weight: 700; color: var(--primary);
+}
+
+.btn-logout {
+ padding: 6px 14px; background: #fee2e2; color: #dc2626;
+ border: 1px solid #fca5a5; border-radius: 20px; font-family: var(--font);
+ font-size: 12px; font-weight: 600; cursor: pointer; transition: all .2s;
+ text-decoration: none; display: inline-flex; align-items: center;
+}
+.btn-logout:hover { background: #dc2626; color: white; border-color: #dc2626; }
+
+
#roleSwitchBtn {
padding: 6px 14px; background: var(--primary); color: white;
border: none; border-radius: 20px; font-family: var(--font);
@@ -554,4 +570,221 @@ html, body { height: 100%; font-family: var(--font); background: var(--bg); over
/* ============================================
ANIMATIONS
============================================ */
-@keyframes fadeIn { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:translateX(0)} }
\ No newline at end of file
+@keyframes fadeIn { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:translateX(0)} }
+
+/* ============================================
+ USER NAME PILL & LOGOUT
+ ============================================ */
+.user-name-pill {
+ display: flex; align-items: center; gap: 6px;
+ background: var(--primary-light); border: 1.5px solid #99f6e4;
+ border-radius: 20px; padding: 5px 12px;
+ font-size: 12px; font-weight: 600; color: var(--primary);
+}
+
+.btn-logout {
+ padding: 6px 14px; background: white; color: var(--danger);
+ border: 1.5px solid #fca5a5; border-radius: 20px; font-family: var(--font);
+ font-size: 11px; font-weight: 600; cursor: pointer; transition: all .2s;
+}
+.btn-logout:hover { background: var(--danger-light); border-color: var(--danger); }
+
+/* ============================================
+ PAGE: HISTORY PELAPORAN
+ ============================================ */
+#pageHistory { display: none; background: var(--bg); }
+#pageHistory.active-page { display: block; overflow-y: auto; }
+
+.history-page {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 0 20px;
+ min-height: calc(100vh - var(--nav-h));
+}
+
+.history-header {
+ background: white;
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+ padding: 24px 28px 20px;
+ border: 1px solid var(--border);
+ border-top: none;
+ margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ box-shadow: var(--shadow);
+}
+
+.history-title {
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 3px;
+}
+
+.history-sub {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.history-stats {
+ display: flex;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.history-stat {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 10px 16px;
+ text-align: center;
+ min-width: 80px;
+ transition: all .2s;
+}
+
+.history-stat:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow);
+}
+
+.history-stat-num {
+ display: block;
+ font-size: 20px;
+ font-weight: 700;
+ font-family: var(--mono);
+ color: var(--primary);
+ line-height: 1;
+}
+
+.history-stat-label {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: .6px;
+ color: var(--text-muted);
+ margin-top: 3px;
+ display: block;
+}
+
+.history-toolbar {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.history-toolbar .search-input {
+ padding: 10px 14px;
+ font-size: 13px;
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow);
+}
+
+.history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding-bottom: 30px;
+}
+
+/* History card — read-only version of report card */
+.history-card {
+ background: white;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 16px 18px;
+ transition: all .2s;
+ animation: fadeIn .3s;
+ position: relative;
+}
+
+.history-card:hover {
+ box-shadow: var(--shadow);
+ transform: translateY(-1px);
+ border-color: var(--primary);
+}
+
+.history-card-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ gap: 10px;
+}
+
+.history-card-name {
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.history-card-time {
+ font-size: 10px;
+ color: var(--text-muted);
+ font-family: var(--mono);
+ flex-shrink: 0;
+ background: var(--bg);
+ padding: 2px 8px;
+ border-radius: 12px;
+}
+
+.history-card-text {
+ font-size: 13px;
+ color: var(--text);
+ line-height: 1.6;
+ margin-bottom: 10px;
+}
+
+.history-card-img {
+ width: 100%;
+ max-height: 240px;
+ object-fit: cover;
+ border-radius: var(--radius-sm);
+ margin-bottom: 10px;
+ border: 1px solid var(--border);
+}
+
+.history-card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 10px;
+ border-top: 1px solid var(--border);
+}
+
+.history-card-status {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 3px 10px;
+ border-radius: 12px;
+}
+
+.history-card-status.baru { background: #eff6ff; color: #1d4ed8; }
+.history-card-status.ditangani { background: var(--warning-light); color: var(--warning); }
+.history-card-status.selesai { background: var(--success-light); color: var(--success); }
+
+.history-card-readonly {
+ font-size: 10px;
+ color: var(--text-muted);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+/* ============================================
+ RESPONSIVE — History
+ ============================================ */
+@media (max-width: 768px) {
+ .history-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .history-stats {
+ width: 100%;
+ justify-content: space-between;
+ }
+ .history-stat {
+ min-width: 0;
+ flex: 1;
+ padding: 8px 10px;
+ }
+}
\ No newline at end of file
diff --git a/03/verify_user.php b/03/verify_user.php
new file mode 100644
index 0000000..c58950d
--- /dev/null
+++ b/03/verify_user.php
@@ -0,0 +1,41 @@
+ false, 'message' => 'Akses ditolak. Hanya Admin.']);
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ echo json_encode(['success' => false, 'message' => 'Hanya POST']);
+ exit;
+}
+
+$user_id = (int)($_POST['user_id'] ?? 0);
+$action = $_POST['action'] ?? '';
+
+if ($user_id <= 0 || !in_array($action, ['approve', 'reject'])) {
+ echo json_encode(['success' => false, 'message' => 'Parameter tidak valid']);
+ exit;
+}
+
+$newStatus = ($action === 'approve') ? 'approved' : 'rejected';
+
+$stmt = $conn->prepare("UPDATE users SET status = ? WHERE id = ?");
+$stmt->bind_param("si", $newStatus, $user_id);
+
+if ($stmt->execute()) {
+ echo json_encode(['success' => true, 'message' => "Akun berhasil di-" . $newStatus]);
+} else {
+ echo json_encode(['success' => false, 'message' => 'Gagal memperbarui status: ' . $conn->error]);
+}
+
+$stmt->close();
+$conn->close();