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
+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();