c4eb884495
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
458 lines
24 KiB
PHP
458 lines
24 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id" data-theme="light">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>Manajemen Pengguna - WebGIS</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { font-family: 'Plus Jakarta Sans', sans-serif; }
|
|
body { font-weight: 500; background-color: #f8fafc; }
|
|
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen">
|
|
|
|
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
|
|
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
|
|
<div class="flex-1">
|
|
<a href="{{ url('/') }}" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
|
|
<i class="fa-solid fa-house-chimney-crack"></i>
|
|
WebGIS Pemetaan Kemiskinan
|
|
</a>
|
|
</div>
|
|
<div class="flex-none">
|
|
<ul class="menu menu-horizontal px-1 gap-1">
|
|
<li>
|
|
<a href="{{ url('/map') }}" class="font-medium">
|
|
<i class="fa-solid fa-map"></i> Peta Interaktif
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url('/data') }}" class="font-medium">
|
|
<i class="fa-solid fa-table-list"></i> Data Tabular
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url('/dashboard') }}" class="font-medium">
|
|
<i class="fa-solid fa-chart-bar"></i> Dashboard
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ url('/admin/users') }}" class="font-semibold bg-primary/10 text-primary rounded-lg">
|
|
<i class="fa-solid fa-users-gear"></i> Admin
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<!-- User dropdown -->
|
|
<div class="dropdown dropdown-end ml-2">
|
|
<div tabindex="0" role="button" class="btn btn-ghost gap-2">
|
|
<div class="avatar placeholder">
|
|
<div class="bg-primary text-primary-content rounded-full w-8">
|
|
<span class="text-xs font-bold">{{ substr(auth()->user()->name, 0, 1) }}</span>
|
|
</div>
|
|
</div>
|
|
<span class="hidden sm:inline text-sm font-semibold">{{ auth()->user()->name }}</span>
|
|
<i class="fa-solid fa-chevron-down text-xs opacity-50"></i>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-200 mt-2">
|
|
<li class="menu-title">
|
|
<span class="text-xs text-base-content/50 capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? 'user') }}</span>
|
|
</li>
|
|
<li>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="text-error w-full text-left flex items-center gap-2">
|
|
<i class="fa-solid fa-right-from-bracket"></i>
|
|
Keluar
|
|
</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
|
|
<main class="max-w-screen-xl mx-auto px-4 py-8">
|
|
|
|
<!-- Header -->
|
|
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
|
|
<div>
|
|
<h1 class="text-3xl font-bold flex items-center gap-3">
|
|
<i class="fa-solid fa-users-gear text-primary"></i>
|
|
Manajemen Pengguna
|
|
</h1>
|
|
<p class="text-base-content/50 mt-1">Kelola akun pengguna dan hak akses sistem.</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="{{ url('/api/admin/backup') }}" class="btn btn-outline gap-2">
|
|
<i class="fa-solid fa-database text-success"></i> Backup Database
|
|
</a>
|
|
<button onclick="openCreateModal()" class="btn btn-primary shadow-lg gap-2">
|
|
<i class="fa-solid fa-user-plus"></i> Tambah Pengguna
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8" id="statsGrid">
|
|
<!-- Diisi oleh JS -->
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-base-200 overflow-x-auto">
|
|
<table class="table table-zebra w-full">
|
|
<thead>
|
|
<tr class="text-xs font-bold uppercase opacity-50">
|
|
<th>#</th>
|
|
<th>Nama</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th class="text-center">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="bodyUsers"></tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
|
|
|
|
<!-- Modal: Tambah Pengguna -->
|
|
<dialog id="modal_create_user" class="modal">
|
|
<div class="modal-box w-11/12 max-w-md">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i class="fa-solid fa-user-plus mr-2 text-primary"></i>Tambah Pengguna Baru
|
|
</h3>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
|
|
<input id="create_name" type="text" class="input input-bordered w-full" placeholder="Nama pengguna">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Email *</span></label>
|
|
<input id="create_email" type="email" class="input input-bordered w-full" placeholder="email@domain.com">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Password *</span></label>
|
|
<input id="create_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Role *</span></label>
|
|
<select id="create_role" class="select select-bordered w-full">
|
|
<option value="administrator">Administrator</option>
|
|
<option value="pengurus_ibadah">Pengurus Ibadah</option>
|
|
<option value="petugas_pendataan" selected>Petugas Pendataan</option>
|
|
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button onclick="document.getElementById('modal_create_user').close()" class="btn btn-ghost">Batal</button>
|
|
<button onclick="saveCreateUser()" class="btn btn-primary">
|
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
<!-- Modal: Edit Pengguna -->
|
|
<dialog id="modal_edit_user" class="modal">
|
|
<div class="modal-box w-11/12 max-w-md">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i class="fa-solid fa-user-pen mr-2 text-warning"></i>Edit Pengguna
|
|
</h3>
|
|
<input type="hidden" id="edit_user_id">
|
|
<div class="flex flex-col gap-3">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Nama Lengkap *</span></label>
|
|
<input id="edit_name" type="text" class="input input-bordered w-full" placeholder="Nama pengguna">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Email *</span></label>
|
|
<input id="edit_email" type="email" class="input input-bordered w-full" placeholder="email@domain.com">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Role *</span></label>
|
|
<select id="edit_role" class="select select-bordered w-full">
|
|
<option value="administrator">Administrator</option>
|
|
<option value="pengurus_ibadah">Pengurus Ibadah</option>
|
|
<option value="petugas_pendataan">Petugas Pendataan</option>
|
|
<option value="pemangku_kebijakan">Pemangku Kebijakan</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button onclick="document.getElementById('modal_edit_user').close()" class="btn btn-ghost">Batal</button>
|
|
<button onclick="saveEditUser()" class="btn btn-warning">
|
|
<i class="fa-solid fa-floppy-disk mr-1"></i>Simpan Perubahan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
<!-- Modal: Reset Password -->
|
|
<dialog id="modal_reset_password" class="modal">
|
|
<div class="modal-box w-11/12 max-w-md">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i class="fa-solid fa-key mr-2 text-info"></i>Reset Password
|
|
</h3>
|
|
<input type="hidden" id="reset_user_id">
|
|
<p class="text-sm text-base-content/60 mb-4" id="reset_user_label"></p>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Password Baru *</span></label>
|
|
<input id="reset_password" type="password" class="input input-bordered w-full" placeholder="Min. 8 karakter">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-bold">Konfirmasi Password *</span></label>
|
|
<input id="reset_password_confirmation" type="password" class="input input-bordered w-full" placeholder="Ulangi password baru">
|
|
</div>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button onclick="document.getElementById('modal_reset_password').close()" class="btn btn-ghost">Batal</button>
|
|
<button onclick="saveResetPassword()" class="btn btn-info text-white">
|
|
<i class="fa-solid fa-key mr-1"></i>Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
<!-- Modal: Confirm Delete -->
|
|
<dialog id="modal_delete_user" class="modal">
|
|
<div class="modal-box max-w-sm text-center">
|
|
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-trash-can"></i></div>
|
|
<h3 class="font-bold text-xl mb-2">Hapus Pengguna?</h3>
|
|
<p class="text-sm text-base-content/60 mb-6" id="delete_user_label"></p>
|
|
<input type="hidden" id="delete_user_id">
|
|
<div class="flex gap-2 justify-center">
|
|
<button class="btn btn-ghost px-8" onclick="document.getElementById('modal_delete_user').close()">Batal</button>
|
|
<button class="btn btn-error px-8 text-white" onclick="confirmDeleteUser()">Ya, Hapus</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Toast Container -->
|
|
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
|
|
|
|
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
|
|
<script>
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
let usersData = [];
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadUsers);
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
const res = await fetch('{{ url('/api/admin/users') }}', {
|
|
headers: { 'X-CSRF-TOKEN': csrfToken }
|
|
});
|
|
usersData = await res.json();
|
|
renderUsers();
|
|
} catch (e) {
|
|
showToast('Gagal memuat data pengguna', 'error');
|
|
}
|
|
}
|
|
|
|
const roleLabels = {
|
|
administrator: 'Administrator',
|
|
pengurus_ibadah: 'Pengurus Ibadah',
|
|
petugas_pendataan: 'Petugas Pendataan',
|
|
pemangku_kebijakan:'Pemangku Kebijakan',
|
|
};
|
|
|
|
const roleBadge = {
|
|
administrator: 'badge-error',
|
|
pengurus_ibadah: 'badge-success',
|
|
petugas_pendataan: 'badge-warning',
|
|
pemangku_kebijakan:'badge-info',
|
|
};
|
|
|
|
function renderUsers() {
|
|
// Stats
|
|
const counts = { administrator: 0, pengurus_ibadah: 0, petugas_pendataan: 0, pemangku_kebijakan: 0 };
|
|
usersData.forEach(u => { if (counts[u.role] !== undefined) counts[u.role]++; });
|
|
document.getElementById('statsGrid').innerHTML = Object.entries(roleLabels).map(([key, label]) => `
|
|
<div class="stat bg-white shadow rounded-2xl border border-base-200">
|
|
<div class="stat-figure text-3xl opacity-20"><i class="fa-solid fa-user"></i></div>
|
|
<div class="stat-title text-xs uppercase font-bold opacity-50">${label}</div>
|
|
<div class="stat-value text-2xl">${counts[key]}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Table
|
|
const tbody = document.getElementById('bodyUsers');
|
|
tbody.innerHTML = usersData.length ? usersData.map((u, i) => `
|
|
<tr>
|
|
<td class="text-xs font-bold opacity-30">${i + 1}</td>
|
|
<td><div class="font-bold">${esc(u.name)}</div></td>
|
|
<td class="text-sm opacity-70">${esc(u.email)}</td>
|
|
<td>
|
|
<span class="badge badge-sm ${roleBadge[u.role] || 'badge-neutral'} badge-outline capitalize">
|
|
${esc(roleLabels[u.role] || u.role)}
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="flex gap-1 justify-center">
|
|
<button onclick="openEditModal(${u.id})" class="btn btn-xs btn-warning btn-outline" title="Edit">
|
|
<i class="fa-solid fa-pen"></i>
|
|
</button>
|
|
<button onclick="openResetModal(${u.id})" class="btn btn-xs btn-info btn-outline" title="Reset Password">
|
|
<i class="fa-solid fa-key"></i>
|
|
</button>
|
|
<button onclick="openDeleteModal(${u.id})" class="btn btn-xs btn-error btn-outline" title="Hapus">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('') : '<tr><td colspan="5" class="text-center py-10 opacity-30 italic">Belum ada pengguna</td></tr>';
|
|
}
|
|
|
|
// ── CREATE ──────────────────────────────────────────────────
|
|
function openCreateModal() {
|
|
document.getElementById('create_name').value = '';
|
|
document.getElementById('create_email').value = '';
|
|
document.getElementById('create_password').value = '';
|
|
document.getElementById('create_role').value = 'petugas_pendataan';
|
|
document.getElementById('modal_create_user').showModal();
|
|
}
|
|
|
|
async function saveCreateUser() {
|
|
const body = {
|
|
name: document.getElementById('create_name').value,
|
|
email: document.getElementById('create_email').value,
|
|
password: document.getElementById('create_password').value,
|
|
role: document.getElementById('create_role').value,
|
|
};
|
|
const res = await fetch('{{ url('/api/admin/users') }}', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
document.getElementById('modal_create_user').close();
|
|
showToast('Pengguna berhasil ditambahkan', 'success');
|
|
await loadUsers();
|
|
} else {
|
|
showToast(json.message || 'Gagal menambahkan pengguna', 'error');
|
|
}
|
|
}
|
|
|
|
// ── EDIT ────────────────────────────────────────────────────
|
|
function openEditModal(id) {
|
|
const u = usersData.find(u => u.id === id);
|
|
if (!u) return;
|
|
document.getElementById('edit_user_id').value = u.id;
|
|
document.getElementById('edit_name').value = u.name;
|
|
document.getElementById('edit_email').value = u.email;
|
|
document.getElementById('edit_role').value = u.role || 'petugas_pendataan';
|
|
document.getElementById('modal_edit_user').showModal();
|
|
}
|
|
|
|
async function saveEditUser() {
|
|
const id = document.getElementById('edit_user_id').value;
|
|
const body = {
|
|
name: document.getElementById('edit_name').value,
|
|
email: document.getElementById('edit_email').value,
|
|
role: document.getElementById('edit_role').value,
|
|
};
|
|
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
document.getElementById('modal_edit_user').close();
|
|
showToast('Data pengguna diperbarui', 'success');
|
|
await loadUsers();
|
|
} else {
|
|
showToast(json.message || 'Gagal memperbarui data', 'error');
|
|
}
|
|
}
|
|
|
|
// ── RESET PASSWORD ──────────────────────────────────────────
|
|
function openResetModal(id) {
|
|
const u = usersData.find(u => u.id === id);
|
|
if (!u) return;
|
|
document.getElementById('reset_user_id').value = u.id;
|
|
document.getElementById('reset_user_label').textContent = `Reset password untuk: ${u.name} (${u.email})`;
|
|
document.getElementById('reset_password').value = '';
|
|
document.getElementById('reset_password_confirmation').value = '';
|
|
document.getElementById('modal_reset_password').showModal();
|
|
}
|
|
|
|
async function saveResetPassword() {
|
|
const id = document.getElementById('reset_user_id').value;
|
|
const body = {
|
|
password: document.getElementById('reset_password').value,
|
|
password_confirmation: document.getElementById('reset_password_confirmation').value,
|
|
};
|
|
const res = await fetch(`{{ url('/api/admin/users') }}/${id}/password`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
document.getElementById('modal_reset_password').close();
|
|
showToast('Password berhasil direset', 'success');
|
|
} else {
|
|
showToast(json.message || 'Gagal reset password', 'error');
|
|
}
|
|
}
|
|
|
|
// ── DELETE ──────────────────────────────────────────────────
|
|
function openDeleteModal(id) {
|
|
const u = usersData.find(u => u.id === id);
|
|
if (!u) return;
|
|
document.getElementById('delete_user_id').value = u.id;
|
|
document.getElementById('delete_user_label').textContent = `Pengguna "${u.name}" (${u.email}) akan dihapus secara permanen.`;
|
|
document.getElementById('modal_delete_user').showModal();
|
|
}
|
|
|
|
async function confirmDeleteUser() {
|
|
const id = document.getElementById('delete_user_id').value;
|
|
const res = await fetch(`{{ url('/api/admin/users') }}/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
document.getElementById('modal_delete_user').close();
|
|
showToast('Pengguna dihapus', 'success');
|
|
await loadUsers();
|
|
} else {
|
|
showToast(json.message || 'Gagal menghapus pengguna', 'error');
|
|
}
|
|
}
|
|
|
|
// ── TOAST ──────────────────────────────────────────────────
|
|
function showToast(msg, type = 'info') {
|
|
const t = document.createElement('div');
|
|
const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
|
|
t.className = `alert ${cls} shadow-lg py-3 text-xs font-bold`;
|
|
t.innerHTML = `<span><i class="fa-solid fa-circle-info mr-2"></i>${msg}</span>`;
|
|
document.getElementById('toastBox').appendChild(t);
|
|
setTimeout(() => t.remove(), 3000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|