feat: add permohonan transfer page with approve/reject workflow and nav links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
GuavaPopper
2026-06-05 05:01:40 +07:00
parent 64f3427d4e
commit 66cfbbd523
7 changed files with 272 additions and 0 deletions
+1
View File
@@ -28,6 +28,7 @@
<li><a href="{{ url('/map') }}" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
<li><a href="{{ url('/data') }}" class="font-medium"><i class="fa-solid fa-table-list"></i> Data</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('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
<li><a href="{{ url('/admin/users') }}" class="font-medium"><i class="fa-solid fa-users-gear"></i> Admin</a></li>
<li><a href="{{ url('/admin/logs') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-clock-rotate-left"></i> Log</a></li>
<li><a href="{{ url('/admin/settings') }}" class="font-medium"><i class="fa-solid fa-sliders"></i> Pengaturan</a></li>
+1
View File
@@ -27,6 +27,7 @@
<li><a href="{{ url('/map') }}" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
<li><a href="{{ url('/data') }}" class="font-medium"><i class="fa-solid fa-table-list"></i> Data</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('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
<li><a href="{{ url('/admin/users') }}" class="font-medium"><i class="fa-solid fa-users-gear"></i> Admin</a></li>
<li><a href="{{ url('/admin/logs') }}" class="font-medium"><i class="fa-solid fa-clock-rotate-left"></i> Log</a></li>
<li><a href="{{ url('/admin/settings') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-sliders"></i> Pengaturan</a></li>
+1
View File
@@ -44,6 +44,7 @@
<i class="fa-solid fa-chart-bar"></i> Dashboard
</a>
</li>
<li><a href="{{ url('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</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
+1
View File
@@ -46,6 +46,7 @@
<i class="fa-solid fa-chart-bar"></i> Dashboard
</a>
</li>
<li><a href="{{ url('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
@if(auth()->user()->hasRole('administrator'))
<li>
<a href="{{ url('/admin/users') }}" class="font-medium">
+1
View File
@@ -41,6 +41,7 @@
<i class="fa-solid fa-chart-bar"></i> Dashboard
</a>
</li>
<li><a href="{{ url('/permohonan') }}" class="font-medium"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
@if(auth()->user()->hasRole('administrator'))
<li>
<a href="{{ url('/admin/users') }}" class="font-medium">
+5
View File
@@ -93,6 +93,11 @@
<i class="fa-solid fa-chart-bar"></i> Dashboard
</a>
</li>
<li>
<a href="{{ url('/permohonan') }}" class="font-medium">
<i class="fa-solid fa-right-left"></i> Transfer
</a>
</li>
@if(auth()->user()->hasRole('administrator'))
<li>
<a href="{{ url('/admin/users') }}" class="font-medium">
+262
View File
@@ -0,0 +1,262 @@
<!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>Permohonan Transfer - 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 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, .btn { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
</head>
<body class="min-h-screen">
<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</a></li>
<li><a href="{{ url('/data') }}" class="font-medium"><i class="fa-solid fa-table-list"></i> Data</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('/permohonan') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-right-left"></i> Transfer</a></li>
@if(auth()->user()->hasRole('administrator'))
<li><a href="{{ url('/admin/users') }}" class="font-medium"><i class="fa-solid fa-users-gear"></i> Admin</a></li>
<li><a href="{{ url('/admin/logs') }}" class="font-medium"><i class="fa-solid fa-clock-rotate-left"></i> Log</a></li>
<li><a href="{{ url('/admin/settings') }}" class="font-medium"><i class="fa-solid fa-sliders"></i> Pengaturan</a></li>
@endif
</ul>
<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>
</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 class="max-w-screen-xl mx-auto px-4 py-8">
<div class="mb-8 flex items-start justify-between flex-wrap gap-4">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3"><i class="fa-solid fa-right-left text-primary"></i> Permohonan Transfer</h1>
<p class="text-base-content/50 mt-1">Permohonan alih tanggungan keluarga miskin antar rumah ibadah.</p>
</div>
@if(auth()->user()->hasRole('administrator') || auth()->user()->hasRole('pengurus_ibadah'))
<button onclick="document.getElementById('modal_ajukan').showModal()" class="btn btn-primary gap-2">
<i class="fa-solid fa-plus"></i> Ajukan Permohonan
</button>
@endif
</div>
<!-- Filter status -->
<div class="flex gap-2 mb-4 flex-wrap">
<button onclick="filterStatus('semua')" class="btn btn-sm btn-active" id="filter_semua">Semua</button>
<button onclick="filterStatus('pending')" class="btn btn-sm" id="filter_pending"><i class="fa-solid fa-clock mr-1"></i>Pending</button>
<button onclick="filterStatus('disetujui')" class="btn btn-sm" id="filter_disetujui"><i class="fa-solid fa-check mr-1"></i>Disetujui</button>
<button onclick="filterStatus('ditolak')" class="btn btn-sm" id="filter_ditolak"><i class="fa-solid fa-xmark mr-1"></i>Ditolak</button>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-base-200 overflow-x-auto">
<table class="table table-sm w-full">
<thead>
<tr class="text-xs uppercase opacity-50">
<th>Dari</th><th>Ke</th><th>Keluarga Miskin</th><th>Alasan</th><th>Status</th><th>Diajukan</th><th>Catatan Respons</th>
@if(auth()->user()->hasRole('administrator') || auth()->user()->hasRole('pengurus_ibadah'))
<th class="text-center">Aksi</th>
@endif
</tr>
</thead>
<tbody id="bodyPermohonan"><tr><td colspan="8" class="text-center py-10 opacity-30 italic">Memuat...</td></tr></tbody>
</table>
</div>
</main>
<!-- Modal Ajukan Permohonan -->
<dialog id="modal_ajukan" class="modal">
<div class="modal-box w-11/12 max-w-lg">
<h3 class="font-bold text-lg mb-4"><i class="fa-solid fa-right-left mr-2 text-primary"></i>Ajukan Permohonan Transfer</h3>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">Dari Rumah Ibadah</span></label>
<select id="perm_dari" class="select select-bordered w-full"></select>
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">Ke Rumah Ibadah (Tujuan)</span></label>
<select id="perm_ke" class="select select-bordered w-full"></select>
</div>
<div class="form-control mb-3">
<label class="label"><span class="label-text font-bold">Keluarga Miskin</span></label>
<select id="perm_miskin" class="select select-bordered w-full"></select>
</div>
<div class="form-control mb-4">
<label class="label"><span class="label-text font-bold">Alasan Permohonan</span></label>
<textarea id="perm_alasan" class="textarea textarea-bordered w-full" rows="3" placeholder="Jelaskan alasan transfer..."></textarea>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_ajukan').close()" class="btn btn-ghost">Batal</button>
<button onclick="ajukanTransfer()" class="btn btn-primary"><i class="fa-solid fa-paper-plane mr-1"></i>Kirim Permohonan</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<!-- Modal Respons -->
<dialog id="modal_respons" class="modal">
<div class="modal-box w-11/12 max-w-md">
<h3 class="font-bold text-lg mb-4" id="respons_title">Proses Permohonan</h3>
<input type="hidden" id="respons_id">
<input type="hidden" id="respons_status">
<div class="form-control mb-4">
<label class="label"><span class="label-text font-bold">Catatan Respons (opsional)</span></label>
<textarea id="respons_catatan" class="textarea textarea-bordered w-full" rows="3" placeholder="Catatan untuk pengaju..."></textarea>
</div>
<div class="modal-action">
<button onclick="document.getElementById('modal_respons').close()" class="btn btn-ghost">Batal</button>
<button onclick="submitRespons()" class="btn btn-primary" id="btnSubmitRespons">Konfirmasi</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
const statusBadge = { pending: 'badge-warning', disetujui: 'badge-success', ditolak: 'badge-error' };
const canRespond = {{ auth()->user()->hasRole('administrator') || auth()->user()->hasRole('pengurus_ibadah') ? 'true' : 'false' }};
let allPermohonan = [];
let ibadahList = [];
let miskinList = [];
let activeFilter = 'semua';
document.addEventListener('DOMContentLoaded', async () => {
const [permRes, ibadahRes, miskinRes] = await Promise.all([
fetch('{{ url('/api/permohonan') }}', { headers: { 'X-CSRF-TOKEN': csrfToken } }),
fetch('{{ url('/api/ibadah') }}'),
fetch('{{ url('/api/miskin') }}'),
]);
allPermohonan = await permRes.json();
ibadahList = await ibadahRes.json();
miskinList = await miskinRes.json();
const optIbadah = ibadahList.map(ib => `<option value="${ib.id}">${esc(ib.nama)}</option>`).join('');
document.getElementById('perm_dari').innerHTML = optIbadah;
document.getElementById('perm_ke').innerHTML = optIbadah;
document.getElementById('perm_miskin').innerHTML = miskinList
.map(m => `<option value="${esc(m.id_rumah)}">${esc(m.id_rumah)} — ${esc(m.nama_kk || '-')}</option>`).join('');
renderPermohonan();
});
function filterStatus(status) {
activeFilter = status;
['semua', 'pending', 'disetujui', 'ditolak'].forEach(s => {
document.getElementById(`filter_${s}`).classList.toggle('btn-active', s === status);
});
renderPermohonan();
}
function renderPermohonan() {
const list = activeFilter === 'semua' ? allPermohonan : allPermohonan.filter(p => p.status === activeFilter);
const colspan = canRespond ? 8 : 7;
document.getElementById('bodyPermohonan').innerHTML = list.length ? list.map(p => `
<tr>
<td class="text-xs font-semibold">${esc(p.dari_ibadah || '-')}</td>
<td class="text-xs font-semibold">${esc(p.ke_ibadah || '-')}</td>
<td class="text-xs"><div class="font-bold">${esc(p.id_rumah)}</div><div class="opacity-50">${esc(p.nama_kk || '-')}</div></td>
<td class="text-xs max-w-[180px] truncate" title="${esc(p.alasan)}">${esc(p.alasan)}</td>
<td><span class="badge badge-sm ${statusBadge[p.status] || ''} badge-outline">${esc(p.status)}</span></td>
<td class="text-xs opacity-60">${esc(p.diajukan_oleh || '-')}<br><span class="text-[10px]">${esc(p.created_at || '')}</span></td>
<td class="text-xs">${p.catatan_respons ? esc(p.catatan_respons) : '<span class="opacity-30">-</span>'}</td>
${canRespond ? `<td class="text-center">
${p.status === 'pending' ? `<div class="flex gap-1 justify-center">
<button onclick="openRespons(${p.id}, 'disetujui')" class="btn btn-xs btn-success btn-outline" title="Setujui"><i class="fa-solid fa-check"></i></button>
<button onclick="openRespons(${p.id}, 'ditolak')" class="btn btn-xs btn-error btn-outline" title="Tolak"><i class="fa-solid fa-xmark"></i></button>
</div>` : ''}
</td>` : ''}
</tr>`).join('')
: `<tr><td colspan="${colspan}" class="text-center py-10 opacity-30 italic">Belum ada permohonan</td></tr>`;
}
async function ajukanTransfer() {
const body = {
dari_ibadah_id: parseInt(document.getElementById('perm_dari').value),
ke_ibadah_id: parseInt(document.getElementById('perm_ke').value),
id_rumah: document.getElementById('perm_miskin').value,
alasan: document.getElementById('perm_alasan').value.trim(),
};
if (!body.alasan) { showToast('Alasan wajib diisi', 'error'); return; }
if (body.dari_ibadah_id === body.ke_ibadah_id) { showToast('Ibadah asal dan tujuan harus berbeda', 'error'); return; }
const res = await fetch('{{ url('/api/permohonan') }}', {
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_ajukan').close();
showToast('Permohonan diajukan', 'success');
location.reload();
} else {
showToast(json.message || 'Gagal mengajukan', 'error');
}
}
function openRespons(id, status) {
document.getElementById('respons_id').value = id;
document.getElementById('respons_status').value = status;
document.getElementById('respons_title').textContent = status === 'disetujui' ? '✅ Setujui Permohonan' : '❌ Tolak Permohonan';
const btn = document.getElementById('btnSubmitRespons');
btn.className = `btn ${status === 'disetujui' ? 'btn-success' : 'btn-error'} text-white`;
document.getElementById('respons_catatan').value = '';
document.getElementById('modal_respons').showModal();
}
async function submitRespons() {
const id = document.getElementById('respons_id').value;
const status = document.getElementById('respons_status').value;
const body = {
status,
catatan_respons: document.getElementById('respons_catatan').value.trim() || null,
};
const res = await fetch(`{{ url('/api/permohonan') }}/${id}/respond`, {
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_respons').close();
showToast(`Permohonan ${esc(status)}`, 'success');
location.reload();
} else {
showToast(json.message || 'Gagal memproses', 'error');
}
}
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>${esc(msg)}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
</script>
</body>
</html>