Update Project 3 files

This commit is contained in:
2026-06-10 22:26:03 +07:00
parent 0ca1af7ede
commit 7bb07f3335
17 changed files with 2169 additions and 79 deletions
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* admin_reset_password.php — Admin mereset password user
* POST: request_id, user_id, new_password
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'admin') {
echo json_encode(['success' => 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();
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* auth_check.php — Cek Session Aktif
*/
session_start();
header('Content-Type: application/json');
if (isset($_SESSION['user_id'])) {
echo json_encode([
'logged_in' => 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]);
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* auth_login.php — Login User
* POST: email, password
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => 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();
+18
View File
@@ -0,0 +1,18 @@
<?php
/**
* auth_logout.php — Logout User
* Supports both AJAX (POST) and direct navigation (GET)
*/
session_start();
session_unset();
session_destroy();
// If called via AJAX (POST), return JSON
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
} else {
// If navigated directly, redirect to login
header('Location: login.html');
exit;
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* auth_register.php — Daftar Akun Baru
* POST: nama, email, password, role
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => 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();
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* get_laporan_user.php — Ambil semua laporan (untuk history masyarakat)
* Masyarakat bisa melihat semua laporan tapi read-only
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => 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();
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* get_pending_users.php — Ambil daftar user yang statusnya 'pending'
* Hanya admin yang boleh akses
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'admin') {
echo json_encode(['success' => 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();
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* get_reset_requests.php — Ambil daftar permintaan reset password (Admin only)
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'admin') {
echo json_encode(['success' => 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();
+99 -53
View File
@@ -1,3 +1,11 @@
<?php
session_start();
// Cek jika belum login, arahkan langsung ke login.html untuk mencegah flicker
if (!isset($_SESSION['user_id'])) {
header('Location: login.html');
exit;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
@@ -35,67 +43,36 @@
<span class="nav-item-label">Pelaporan</span>
<span class="nav-badge hidden" id="navReportBadge">0</span>
</button>
<button class="nav-item" id="navHistory" onclick="navigateTo('history')" style="display:none">
<span class="nav-item-icon">📜</span>
<span class="nav-item-label">History</span>
</button>
<button class="nav-item" id="navVerify" onclick="openVerifyModal()" style="display:none">
<span class="nav-item-icon">🛡️</span>
<span class="nav-item-label">Verifikasi Akun</span>
</button>
</div>
<!-- Kanan: Role switcher -->
<!-- Kanan: User info tampilkan data session dari PHP -->
<div class="nav-right">
<div id="roleIndicator" class="role-pill">
<span id="roleIcon">👑</span>
<span id="roleLabel">Admin</span>
<span id="roleIcon"><?php
$roleIcons = ['admin'=>'👑','surveyer'=>'📋','viewer'=>'👁'];
echo $roleIcons[$_SESSION['user_role'] ?? ''] ?? '👤';
?></span>
<span id="roleLabel"><?php
$roleLabels = ['admin'=>'Admin','surveyer'=>'Surveyer','viewer'=>'Masyarakat'];
echo htmlspecialchars($roleLabels[$_SESSION['user_role'] ?? ''] ?? 'Guest');
?></span>
</div>
<button id="roleSwitchBtn" onclick="openRoleModal()">Ganti Role </button>
<div id="userNamePill" class="user-name-pill">
<span id="userNameDisplay"><?php echo htmlspecialchars($_SESSION['user_nama'] ?? ''); ?></span>
</div>
<a href="auth_logout.php" id="btnLogout" class="btn-logout" onclick="return confirm('Yakin ingin keluar?')">🚪 Keluar</a>
</div>
</nav>
<!-- =====================================================================
ROLE MODAL
===================================================================== -->
<div id="roleModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:420px">
<div class="modal-header">
<div class="modal-title">🔐 Pilih Role Pengguna</div>
<button class="modal-close" onclick="closeRoleModal()"></button>
</div>
<div class="modal-body" style="padding:16px">
<p style="font-size:12px;color:var(--text-muted);margin-bottom:14px">Pilih role sesuai hak akses Anda. Perubahan langsung berlaku.</p>
<div class="role-cards">
<div class="role-card" id="roleCard_admin" onclick="setRole('admin')">
<div class="role-card-icon">👑</div>
<div class="role-card-name">Admin</div>
<div class="role-card-desc">Akses penuh tambah, edit, hapus, lihat semua data dan laporan</div>
<div class="role-card-perms">
<span class="perm perm-green"> Tambah & Edit</span>
<span class="perm perm-green"> Hapus & Reset</span>
<span class="perm perm-green"> Lihat laporan</span>
<span class="perm perm-green"> Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_surveyer" onclick="setRole('surveyer')">
<div class="role-card-icon">📋</div>
<div class="role-card-name">Surveyer</div>
<div class="role-card-desc">Input data & kelola laporan tidak bisa hapus atau reset</div>
<div class="role-card-perms">
<span class="perm perm-green"> Tambah & Edit</span>
<span class="perm perm-red"> Hapus & Reset</span>
<span class="perm perm-green"> Lihat laporan</span>
<span class="perm perm-green"> Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_viewer" onclick="setRole('viewer')">
<div class="role-card-icon">👁</div>
<div class="role-card-name">Viewer</div>
<div class="role-card-desc">Hanya lihat peta & kirim laporan masyarakat</div>
<div class="role-card-perms">
<span class="perm perm-green"> Lihat peta</span>
<span class="perm perm-red"> Edit data</span>
<span class="perm perm-red"> Lihat laporan</span>
<span class="perm perm-green"> Kirim laporan</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Role modal removed role is now from database/session -->
<!-- =====================================================================
PAGE: PETA (default)
@@ -309,6 +286,44 @@
</div>
</div><!-- end #pagePelaporan -->
<!-- =====================================================================
PAGE: HISTORY PELAPORAN (Masyarakat read-only)
===================================================================== -->
<div id="pageHistory" class="page">
<div class="history-page">
<div class="history-header">
<div class="history-header-content">
<div class="history-title">📜 History Pelaporan</div>
<div class="history-sub">Riwayat semua laporan yang telah dikirim oleh masyarakat</div>
</div>
<div class="history-stats">
<div class="history-stat">
<span class="history-stat-num" id="historyTotal">0</span>
<span class="history-stat-label">Total Laporan</span>
</div>
<div class="history-stat">
<span class="history-stat-num" id="historyBaru">0</span>
<span class="history-stat-label">Baru</span>
</div>
<div class="history-stat">
<span class="history-stat-num" id="historyDitangani">0</span>
<span class="history-stat-label">Ditangani</span>
</div>
<div class="history-stat">
<span class="history-stat-num" id="historySelesai">0</span>
<span class="history-stat-label">Selesai</span>
</div>
</div>
</div>
<div class="history-toolbar">
<input type="text" id="searchHistory" class="search-input" placeholder="🔍 Cari laporan..." oninput="filterHistory()" style="flex:1"/>
</div>
<div class="history-list" id="historyList">
<div class="empty-state" style="margin-top:40px">📭 Belum ada laporan.</div>
</div>
</div>
</div><!-- end #pageHistory -->
<!-- =====================================================================
CUSTOM DATE PICKER
@@ -404,6 +419,37 @@
</div>
</div>
<!-- =====================================================================
MODAL VERIFIKASI AKUN (Admin)
===================================================================== -->
<div id="verifyModal" class="modal-overlay hidden">
<div class="modal-box modal-wide" style="max-width: 620px;">
<div class="modal-header">
<div class="modal-title">🛡️ Panel Admin</div>
<button class="modal-close" onclick="closeVerifyModal()"></button>
</div>
<!-- Tabs -->
<div style="display:flex; border-bottom:1px solid var(--border); background:#fafafa;">
<button id="vTabVerify" onclick="switchAdminTab('verify')" style="flex:1;padding:11px;border:none;background:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;color:var(--primary);border-bottom:3px solid var(--primary);">Verifikasi Akun</button>
<button id="vTabReset" onclick="switchAdminTab('reset')" style="flex:1;padding:11px;border:none;background:none;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;color:var(--text-muted);border-bottom:3px solid transparent;">🔑 Reset Password <span id="resetBadge" style="display:none;background:var(--orange);color:white;border-radius:10px;padding:1px 7px;font-size:10px;margin-left:4px;">0</span></button>
</div>
<div class="modal-body" style="max-height: 400px; overflow-y: auto;">
<!-- Panel Verifikasi Akun -->
<div id="adminPanelVerify">
<div id="verifyListContainer">
<div class="empty-state">Memuat data...</div>
</div>
</div>
<!-- Panel Reset Password -->
<div id="adminPanelReset" style="display:none;">
<div id="resetListContainer">
<div class="empty-state">Memuat data...</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="script.js"></script>
</body>
+6 -5
View File
@@ -1,11 +1,12 @@
<?php
define('DB_HOST', 'ys0kkgokgswgoc8ow0wgks0s');
define('DB_USER', 'mysql');
define('DB_PASS', '2DbtID7JC92R3imZjAvKFXYSISCLhsltWFROPQu6yZAlMZ3iXReXpJcygC3lnDZ2');
define('DB_NAME', 'default');
// Coba koneksi remote terlebih dahulu
$conn = @new mysqli('ys0kkgokgswgoc8ow0wgks0s', 'mysql', '2DbtID7JC92R3imZjAvKFXYSISCLhsltWFROPQu6yZAlMZ3iXReXpJcygC3lnDZ2', 'default');
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// Fallback ke localhost (XAMPP) jika remote gagal
if ($conn->connect_error) {
$conn = new mysqli('localhost', 'root', '', 'webgis_bansos');
}
if ($conn->connect_error) {
http_response_code(500);
+1007
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
<?php
/**
* request_reset.php — Kirim permintaan reset password ke Admin
* POST: email
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => 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();
+369 -19
View File
@@ -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 = '<div class="empty-state">Memuat data...</div>';
try {
const res = await fetch('get_pending_users.php');
const data = await res.json();
if (data.success) {
if (data.data.length === 0) {
container.innerHTML = '<div class="empty-state">✅ Tidak ada akun yang menunggu verifikasi.</div>';
} else {
let html = '<div style="display:flex; flex-direction:column; gap:10px; padding:4px 0;">';
data.data.forEach(u => {
const roleLabel = ROLE_CONFIG[u.role] ? ROLE_CONFIG[u.role].label : u.role;
html += `
<div style="border:1px solid var(--border); border-radius:8px; padding:12px; display:flex; justify-content:space-between; align-items:center;">
<div>
<div style="font-weight:700; font-size:14px;">${u.nama}</div>
<div style="font-size:12px; color:var(--text-muted);">${u.email} &bull; <span style="font-weight:600; color:var(--primary);">${roleLabel}</span></div>
<div style="font-size:10px; color:var(--text-muted); margin-top:4px;">🕒 Terdaftar: ${u.waktu}</div>
</div>
<div style="display:flex; gap:8px;">
<button onclick="verifyUserAction(${u.id}, 'reject')" style="background:var(--danger-light); color:var(--danger); border:1px solid #fca5a5; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px;">Tolak</button>
<button onclick="verifyUserAction(${u.id}, 'approve')" style="background:var(--success-light); color:var(--success); border:1px solid #86efac; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px;">Setujui</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
} else {
container.innerHTML = `<div class="empty-state" style="color:red;">Gagal memuat: ${data.message}</div>`;
}
} catch (e) {
container.innerHTML = `<div class="empty-state" style="color:red;">Error mengambil data.</div>`;
}
}
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 = '<div class="empty-state">Memuat data...</div>';
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 = '<div class="empty-state">✅ Tidak ada permintaan reset password.</div>';
} else {
let html = '<div style="display:flex; flex-direction:column; gap:12px; padding:4px 0;">';
data.data.forEach(r => {
const roleLabel = ROLE_CONFIG[r.role] ? ROLE_CONFIG[r.role].label : r.role;
html += `
<div style="border:1px solid #fde68a; border-radius:8px; padding:14px; background:#fffbeb;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;">
<div>
<div style="font-weight:700; font-size:14px;">🔑 ${r.nama}</div>
<div style="font-size:12px; color:var(--text-muted);">${r.email} &bull; <span style="font-weight:600; color:var(--primary);">${roleLabel}</span></div>
<div style="font-size:10px; color:var(--text-muted); margin-top:3px;">🕒 Diminta: ${r.waktu}</div>
</div>
</div>
<div style="display:flex; gap:8px; align-items:center;">
<div style="position:relative; flex:1;">
<input type="password" id="newPw_${r.id}" placeholder="Password baru (min. 6 karakter)" style="width:100%; padding:8px 30px 8px 10px; border:1.5px solid #d97706; border-radius:6px; font-family:inherit; font-size:12px; outline:none; box-sizing:border-box;"/>
<button onclick="toggleAdminResetPassword(${r.id})" id="togglePw_${r.id}" style="position:absolute; right:8px; top:50%; transform:translateY(-50%); background:none; border:none; cursor:pointer; font-size:14px; color:var(--text-muted);">👁</button>
</div>
<button onclick="adminDoReset(${r.id}, ${r.user_id})" style="background:#d97706; color:white; border:none; padding:8px 14px; border-radius:6px; cursor:pointer; font-weight:700; font-size:12px; white-space:nowrap;">Reset Password</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
} else {
container.innerHTML = `<div class="empty-state" style="color:red;">Gagal: ${data.message}</div>`;
}
} catch (e) {
container.innerHTML = `<div class="empty-state" style="color:red;">Error mengambil data.</div>`;
}
}
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 = '<div class="empty-state" style="margin-top:40px">📭 Tidak ada laporan yang ditemukan.</div>';
return;
}
const statusLabels = {
baru: '🆕 Baru',
ditangani: '🔄 Ditangani',
selesai: '✅ Selesai'
};
container.innerHTML = list.map(r => {
const statusClass = r.status || 'baru';
return `
<div class="history-card">
<div class="history-card-top">
<div class="history-card-name">👤 ${r.name || 'Anonim'}</div>
<div class="history-card-time">🕐 ${r.time || ''}</div>
</div>
<div class="history-card-text">${r.text || ''}</div>
${r.imgBase64 ? `<img class="history-card-img" src="${r.imgBase64}" alt="Foto laporan"/>` : ''}
<div class="history-card-footer">
<div class="history-card-status ${statusClass}">${statusLabels[statusClass] || '🆕 Baru'}</div>
<div class="history-card-readonly">👁 Hanya Lihat</div>
</div>
</div>`;
}).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();
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* setup_users.php — Buat tabel users (jalankan 1x saja)
*/
require_once 'koneksi.php';
$sql = "CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin', 'surveyer', 'viewer') DEFAULT 'viewer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
if ($conn->query($sql)) {
echo json_encode(['success' => true, 'message' => 'Tabel users berhasil dibuat!']);
} else {
echo json_encode(['success' => false, 'message' => 'Gagal: ' . $conn->error]);
}
$conn->close();
+10 -1
View File
@@ -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'] ?? '';
+234 -1
View File
@@ -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)} }
@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;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* verify_user.php — Approve atau Reject user yang pending
* POST: user_id, action (approve/reject)
* Hanya admin yang boleh akses
*/
session_start();
require_once 'koneksi.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'admin') {
echo json_encode(['success' => 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();