Update Project 3 files
This commit is contained in:
+369
-19
@@ -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} • <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} • <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();
|
||||
|
||||
Reference in New Issue
Block a user