From 34816455cbaaec83c62943859792055f87a70586 Mon Sep 17 00:00:00 2001 From: Irwan Cahyono Date: Mon, 4 May 2026 16:10:29 +0700 Subject: [PATCH] pwa Co-authored-by: Copilot --- .../Web/Ekosistem/PetaniCPMController.php | 93 ++++++++++++++++++ public/icons/agromonev_192.png | Bin 0 -> 2210 bytes public/icons/agromonev_512.png | Bin 0 -> 5335 bytes public/manifest.json | 20 ++++ public/service-worker.js | 73 ++++++++++++++ resources/views/layout/header.blade.php | 9 +- resources/views/layout/main.blade.php | 10 ++ resources/views/offline.blade.php | 10 ++ routes/web.php | 2 + 9 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 public/icons/agromonev_192.png create mode 100644 public/icons/agromonev_512.png create mode 100644 public/manifest.json create mode 100644 public/service-worker.js create mode 100644 resources/views/offline.blade.php diff --git a/app/Http/Controllers/Web/Ekosistem/PetaniCPMController.php b/app/Http/Controllers/Web/Ekosistem/PetaniCPMController.php index e67b130..b5d25e2 100644 --- a/app/Http/Controllers/Web/Ekosistem/PetaniCPMController.php +++ b/app/Http/Controllers/Web/Ekosistem/PetaniCPMController.php @@ -8,6 +8,7 @@ use Illuminate\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\Http\RedirectResponse; use Illuminate\Validation\Rule; +use Illuminate\Http\JsonResponse; use App\Models\Ekosistem\PetaniCPM; use App\Models\Wilayah\Provinsi; @@ -184,4 +185,96 @@ class PetaniCPMController extends Controller return redirect()->route('petani-cpm.index')->with('success', 'Data petani CPM berhasil dihapus.'); } + + public function summary(string $tipe): JsonResponse + { + switch ($tipe) { + case 'jenis_kelamin': + return $this->calculateJenisKelamin(); + case 'usia': + return $this->calculateUsia(); + case 'pendidikan': + return $this->calculatePendidikan(); + } + + return response()->json([ + 'petani' => PetaniCPM::count(), + 'pengumpul' => PetaniCPM::where('petani_pengumpul', 'pengumpul')->count(), + 'petani' => PetaniCPM::where('petani_pengumpul', 'petani')->count(), + ]); + } + + private function calculateJenisKelamin() + { + $petaniBase = PetaniCPM::query(); + + return response()->json([ + 'total' => (clone $petaniBase)->count(), + 'laki_laki' => (clone $petaniBase)->where('jenis_kelamin', 1)->count(), + 'perempuan' => (clone $petaniBase)->where('jenis_kelamin', 0)->count(), + ]); + } + + private function calculateUsia() + { + $petaniBase = PetaniCPM::query(); + + $usiaRataRata = (clone $petaniBase) + ->selectRaw('AVG(EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir))) as avg') + ->value('avg'); + $usiaMayoritas = (clone $petaniBase) + ->selectRaw(" + CASE + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 0 AND 4 THEN '0-4' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 5 AND 9 THEN '5-9' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 10 AND 14 THEN '10-14' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 15 AND 19 THEN '15-19' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 20 AND 24 THEN '20-24' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 25 AND 29 THEN '25-29' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 30 AND 34 THEN '30-34' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 35 AND 39 THEN '35-39' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 40 AND 44 THEN '40-44' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 45 AND 49 THEN '45-49' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 50 AND 54 THEN '50-54' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 55 AND 59 THEN '55-59' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 60 AND 64 THEN '60-64' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 65 AND 69 THEN '65-69' + WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 70 AND 74 THEN '70-74' + ELSE '75+' + END as kelompok_usia + ") + ->groupBy('kelompok_usia') + ->orderByRaw('COUNT(*) DESC') + ->value('kelompok_usia'); + + return response()->json([ + 'usia_rata_rata' => $usiaRataRata ? round($usiaRataRata, 1) : 0, + 'usia_mayoritas' => $usiaMayoritas ?? '-', + ]); + } + + private function calculatePendidikan() + { + $petaniBase = PetaniCPM::query(); + + $mayoritasValue = (clone $petaniBase) + ->selectRaw('pendidikan_terakhir, COUNT(*) as total') + ->groupBy('pendidikan_terakhir') + ->orderByDesc('total') + ->orderByDesc('pendidikan_terakhir') + ->value('pendidikan_terakhir'); + + $tertinggiValue = (clone $petaniBase) + ->whereNotNull('pendidikan_terakhir') + ->orderByDesc('pendidikan_terakhir') // karena ada prefix angka + ->value('pendidikan_terakhir'); + + $mayoritasLabel = $mayoritasValue?->label() ?? '-'; + $tertinggiLabel = $tertinggiValue?->label() ?? '-'; + + return response()->json([ + 'pendidikan_mayoritas' => $mayoritasLabel, + 'pendidikan_tertinggi' => $tertinggiLabel, + ]); + } } diff --git a/public/icons/agromonev_192.png b/public/icons/agromonev_192.png new file mode 100644 index 0000000000000000000000000000000000000000..a233804192a5d2bf072f22b7f553f27a56db67da GIT binary patch literal 2210 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 literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..dd5cf48 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "AgroMonev", + "short_name": "AgroMonev", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#0d6efd", + "icons": [ + { + "src": "/icons/agromonev_192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/agromonev_512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..cae4b14 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,73 @@ +const CACHE_NAME = 'agromonev-v1'; + +// asset statis saja +const STATIC_ASSETS = [ + '/', + '/offline', +]; + +// install +self.addEventListener('install', event => { + self.skipWaiting(); + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(STATIC_ASSETS)) + ); +}); + +// activate (hapus cache lama) +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => { + return Promise.all( + keys.map(key => { + if (key !== CACHE_NAME) { + return caches.delete(key); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', event => { + const request = event.request; + + if (!request.url.startsWith('http')) return; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + + // ❗ SKIP halaman auth & dashboard + if ( + url.pathname.startsWith('/login') || + url.pathname.startsWith('/logout') || + url.pathname.startsWith('/register') || + url.pathname.startsWith('/dashboard') || + url.pathname.startsWith('/admin') + ) { + event.respondWith(fetch(request)); + return; + } + + // ❗ SKIP API + if (url.pathname.startsWith('/api')) { + event.respondWith(fetch(request)); + return; + } + + // 🔹 HTML → network first + if (request.headers.get('accept')?.includes('text/html')) { + event.respondWith( + fetch(request) + .catch(() => caches.match('/offline')) + ); + return; + } + + // 🔹 asset → cache + event.respondWith( + caches.match(request).then(res => res || fetch(request)) + ); +}); \ No newline at end of file diff --git a/resources/views/layout/header.blade.php b/resources/views/layout/header.blade.php index 636279b..b0f3656 100644 --- a/resources/views/layout/header.blade.php +++ b/resources/views/layout/header.blade.php @@ -15,10 +15,17 @@ name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" /> - Admin | {{ $title ?? 'Database Petani' }} + AgroMonev | {{ $title ?? 'Database Petani' }} + + + + + + + diff --git a/resources/views/layout/main.blade.php b/resources/views/layout/main.blade.php index 5e59cbe..036e944 100644 --- a/resources/views/layout/main.blade.php +++ b/resources/views/layout/main.blade.php @@ -112,5 +112,15 @@ } --> @stack('scripts') + + \ No newline at end of file diff --git a/resources/views/offline.blade.php b/resources/views/offline.blade.php new file mode 100644 index 0000000..35703f0 --- /dev/null +++ b/resources/views/offline.blade.php @@ -0,0 +1,10 @@ + + + + Offline + + +

Koneksi terputus

+

Silakan cek internet Anda.

+ + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 1111e25..6fb26ec 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,6 +9,8 @@ Route::post('/create-account', [App\Http\Controllers\Web\Konfigurasi\AuthControl Route::post('/authenticate', [App\Http\Controllers\Web\Konfigurasi\AuthController::class, 'authenticate'])->name('auth.authenticate'); Route::post('/logout', [App\Http\Controllers\Web\Konfigurasi\AuthController::class, 'logout'])->name('logout'); +Route::view('/offline', 'offline')->name('offline'); + Route::middleware('auth') ->group(function () {