From 3ecf7d937c6ba2b4e8e4a589e9edcb55368f0e86 Mon Sep 17 00:00:00 2001 From: GuavaPopper Date: Thu, 4 Jun 2026 14:52:25 +0700 Subject: [PATCH] feat: add data change notifications via 30s polling for all roles Co-Authored-By: Claude Sonnet 4.6 --- resources/views/dashboard.blade.php | 33 +++++++++++++++++++++++++ resources/views/data.blade.php | 36 ++++++++++++++++++++++++++++ resources/views/map.blade.php | 37 +++++++++++++++++++++++++++++ routes/web.php | 13 ++++++++++ 4 files changed, 119 insertions(+) diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index f3384a5..80736ce 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -391,6 +391,39 @@ setter(new Chart(ctx, config)); } + // ── NOTIFIKASI PERUBAHAN DATA ──────────────────────────────────────── + (function initNotifPolling() { + const POLL_MS = 30000; + const KEY_IBADAH = 'notif_latest_ibadah'; + const KEY_MISKIN = 'notif_latest_miskin'; + + async function pollNotifications() { + try { + const since = localStorage.getItem(KEY_IBADAH) || new Date(0).toISOString(); + const res = await fetch( + `{{ url('/api/notifications') }}?since=${encodeURIComponent(since)}`, + { headers: { 'X-CSRF-TOKEN': csrfToken } } + ); + if (!res.ok) return; + const data = await res.json(); + + if (data.new_ibadah > 0 || data.new_miskin > 0) { + if (data.new_ibadah > 0) showToast(`${data.new_ibadah} rumah ibadah baru ditambahkan`, 'info'); + if (data.new_miskin > 0) showToast(`${data.new_miskin} keluarga miskin baru ditambahkan`, 'info'); + if (typeof loadDashboard !== 'undefined') loadDashboard(); + } + if (data.latest_ibadah) localStorage.setItem(KEY_IBADAH, data.latest_ibadah); + if (data.latest_miskin) localStorage.setItem(KEY_MISKIN, data.latest_miskin); + } catch (e) { /* silent */ } + } + + document.addEventListener('DOMContentLoaded', () => { + if (!localStorage.getItem(KEY_IBADAH)) localStorage.setItem(KEY_IBADAH, new Date().toISOString()); + if (!localStorage.getItem(KEY_MISKIN)) localStorage.setItem(KEY_MISKIN, new Date().toISOString()); + setInterval(pollNotifications, POLL_MS); + }); + })(); + function showToast(msg, type = 'info') { const t = document.createElement('div'); const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info'; diff --git a/resources/views/data.blade.php b/resources/views/data.blade.php index e36c7b0..2435643 100644 --- a/resources/views/data.blade.php +++ b/resources/views/data.blade.php @@ -705,6 +705,42 @@ document.getElementById('toastBox').appendChild(t); setTimeout(() => t.remove(), 3000); } + + // ── NOTIFIKASI PERUBAHAN DATA ──────────────────────────────────────── + (function initNotifPolling() { + const POLL_MS = 30000; + const KEY_IBADAH = 'notif_latest_ibadah'; + const KEY_MISKIN = 'notif_latest_miskin'; + + async function pollNotifications() { + try { + const since = localStorage.getItem(KEY_IBADAH) || new Date(0).toISOString(); + const res = await fetch( + `{{ url('/api/notifications') }}?since=${encodeURIComponent(since)}`, + { headers: { 'X-CSRF-TOKEN': csrfToken } } + ); + if (!res.ok) return; + const data = await res.json(); + + if (data.new_ibadah > 0 || data.new_miskin > 0) { + if (data.new_ibadah > 0) showToast(`${data.new_ibadah} rumah ibadah baru ditambahkan`, 'info'); + if (data.new_miskin > 0) showToast(`${data.new_miskin} keluarga miskin baru ditambahkan`, 'info'); + const fn = typeof loadAllData !== 'undefined' ? loadAllData + : typeof loadData !== 'undefined' ? loadData + : null; + if (fn) fn(); + } + if (data.latest_ibadah) localStorage.setItem(KEY_IBADAH, data.latest_ibadah); + if (data.latest_miskin) localStorage.setItem(KEY_MISKIN, data.latest_miskin); + } catch (e) { /* silent */ } + } + + document.addEventListener('DOMContentLoaded', () => { + if (!localStorage.getItem(KEY_IBADAH)) localStorage.setItem(KEY_IBADAH, new Date().toISOString()); + if (!localStorage.getItem(KEY_MISKIN)) localStorage.setItem(KEY_MISKIN, new Date().toISOString()); + setInterval(pollNotifications, POLL_MS); + }); + })(); diff --git a/resources/views/map.blade.php b/resources/views/map.blade.php index fa4ad6c..d75767f 100644 --- a/resources/views/map.blade.php +++ b/resources/views/map.blade.php @@ -1044,6 +1044,43 @@ } } + // ── NOTIFIKASI PERUBAHAN DATA ──────────────────────────────────────── + (function initNotifPolling() { + const POLL_MS = 30000; + const KEY_IBADAH = 'notif_latest_ibadah'; + const KEY_MISKIN = 'notif_latest_miskin'; + + async function pollNotifications() { + try { + const since = localStorage.getItem(KEY_IBADAH) || new Date(0).toISOString(); + const res = await fetch( + `{{ url('/api/notifications') }}?since=${encodeURIComponent(since)}`, + { headers: { 'X-CSRF-TOKEN': csrfToken } } + ); + if (!res.ok) return; + const data = await res.json(); + + if (data.new_ibadah > 0 || data.new_miskin > 0) { + if (data.new_ibadah > 0) showToast(`${data.new_ibadah} rumah ibadah baru ditambahkan`, 'info'); + if (data.new_miskin > 0) showToast(`${data.new_miskin} keluarga miskin baru ditambahkan`, 'info'); + const fn = typeof loadAllData !== 'undefined' ? loadAllData + : typeof loadData !== 'undefined' ? loadData + : typeof loadDashboard !== 'undefined' ? loadDashboard + : null; + if (fn) fn(); + } + if (data.latest_ibadah) localStorage.setItem(KEY_IBADAH, data.latest_ibadah); + if (data.latest_miskin) localStorage.setItem(KEY_MISKIN, data.latest_miskin); + } catch (e) { /* silent */ } + } + + document.addEventListener('DOMContentLoaded', () => { + if (!localStorage.getItem(KEY_IBADAH)) localStorage.setItem(KEY_IBADAH, new Date().toISOString()); + if (!localStorage.getItem(KEY_MISKIN)) localStorage.setItem(KEY_MISKIN, new Date().toISOString()); + setInterval(pollNotifications, POLL_MS); + }); + })(); + // ── TOAST ────────────────────────────────────────────────────────── function showToast(msg, type = 'info') { const t = document.createElement('div'); diff --git a/routes/web.php b/routes/web.php index 733b71b..dff24f0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -33,6 +33,19 @@ Route::middleware('auth')->group(function () { // API routes moved here for session + CSRF middleware Route::prefix('api')->group(function () { + // Notifikasi — semua role authenticated + Route::get('/notifications', function (\Illuminate\Http\Request $request) { + $since = $request->query('since'); + $after = $since ? \Carbon\Carbon::parse($since) : now()->subMinutes(1); + + return response()->json([ + 'new_ibadah' => \App\Models\RumahIbadah::where('created_at', '>', $after)->count(), + 'new_miskin' => \App\Models\RumahMiskin::where('created_at', '>', $after)->count(), + 'latest_ibadah' => \App\Models\RumahIbadah::max('created_at'), + 'latest_miskin' => \App\Models\RumahMiskin::max('created_at'), + ]); + }); + // Rumah Ibadah — read: all roles, write: administrator + pengurus_ibadah Route::get('/ibadah', [IbadahController::class, 'index']); Route::middleware('role:administrator|pengurus_ibadah')->group(function () {