diff --git a/app/Http/Controllers/Web/Dashboard/IndikatorPertanianBackupController.php b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianBackupController.php new file mode 100644 index 0000000..ff39f00 --- /dev/null +++ b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianBackupController.php @@ -0,0 +1,769 @@ +kabupatenOptions(); + $ngadaOption = collect($kabupatenOptions)->first( + fn ($option) => str_contains(strtolower($option['label']), 'ngada') + ); + $initialFilters = $this->selectedFilters($request, $ngadaOption['value'] ?? 'all'); + + $totalPetani = $this->applyPetaniFilters(Petani::query(), $initialFilters)->count(); + $totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $initialFilters)->sum('luas_lahan')) / 10000; + $rataRataLuasLahan = $totalPetani > 0 + ? $totalLuasLahan / $totalPetani + : 0; + $totalProduksiKg = $this->totalProduksiKg($initialFilters); + $rataRataProduksiKg = $totalPetani > 0 + ? $totalProduksiKg / $totalPetani + : 0; + $rataRataProduktivitas = $totalLuasLahan > 0 + ? $totalProduksiKg / $totalLuasLahan + : 0; + + $filterOptions = [ + [ + 'key' => 'kabupaten', + 'label' => 'Kabupaten', + 'options' => $kabupatenOptions, + 'icon' => 'tabler-map-pin', + 'readonly' => true, + ], + [ + 'key' => 'kecamatan', + 'label' => 'Kecamatan', + 'options' => $this->kecamatanOptions(), + ], + [ + 'key' => 'desa', + 'label' => 'Desa', + 'options' => $this->desaOptions(), + ], + [ + 'key' => 'komoditas', + 'label' => 'Komoditas', + 'options' => $this->komoditasOptions(), + ], + [ + 'key' => 'segmentasi', + 'label' => 'Segmentasi', + 'options' => $this->segmentasiOptions(), + ], + ]; + + $summaryData = [ + [ + 'value' => number_format($totalPetani, 0, ',', '.'), + 'label' => 'Petani Terdata', + 'description' => 'Total petani yang masuk sistem', + 'icon' => 'tabler-users', + 'variant' => 'primary', + ], + [ + 'value' => number_format($totalLuasLahan, 2, ',', '.').' ha', + 'label' => 'Total Luas Lahan', + 'description' => 'Rata-rata '.number_format($rataRataLuasLahan, 2, ',', '.').' ha/petani', + 'icon' => 'tabler-mountain', + 'variant' => 'success', + ], + [ + 'value' => number_format($totalProduksiKg, 0, ',', '.').' kg', + 'label' => 'Total Produksi (Semua Komoditas)', + 'description' => 'Rata-rata '.number_format($rataRataProduksiKg, 0, ',', '.').' kg/petani', + 'icon' => 'tabler-wheat', + 'variant' => 'warning', + ], + [ + 'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha', + 'label' => 'Rata-rata Produktivitas', + 'description' => 'Semua komoditas berdasarkan total produksi dan luas lahan', + 'icon' => 'tabler-chart-bar', + 'variant' => 'info', + ], + ]; + + return view('dashboard.indikator-pertanian', [ + 'title' => 'Indikator Pertanian', + 'initialFilters' => $initialFilters, + 'filterOptions' => $filterOptions, + 'summaryData' => $summaryData, + 'chartData' => [ + 'petaniPerKomoditas' => $this->petaniPerKomoditasChart($initialFilters), + 'luasLahanPerKecamatan' => $this->luasLahanPerKecamatanChart($initialFilters), + 'kondisiTanaman' => $this->kondisiTanamanChart($initialFilters), + 'statusKepemilikanLahan' => $this->statusKepemilikanLahanChart($initialFilters), + 'seranganHama' => $this->seranganHamaChart($initialFilters), + 'penyakitTanaman' => $this->penyakitTanamanChart($initialFilters), + 'keparahanSerangan' => $this->keparahanSeranganChart($initialFilters), + 'statusHakKelolaLahan' => $this->statusHakKelolaLahanChart($initialFilters), + 'praktikPemupukan' => $this->praktikPemupukanChart($initialFilters), + 'produksiPerKomoditas' => $this->produksiPerKomoditasChart($initialFilters), + 'produktivitasPerKomoditas' => $this->produktivitasPerKomoditasChart($initialFilters), + 'luasPanenPerKomoditas' => $this->luasPanenPerKomoditasChart($initialFilters), + 'statusPanen' => $this->statusPanenChart($initialFilters), + 'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($initialFilters, 'ha_tahun'), + 'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($initialFilters, 'pohon_tahun'), + ], + ]); + } + + private function selectedFilters(Request $request, string $defaultKabupaten): array + { + $filters = [ + 'kabupaten' => $defaultKabupaten, + 'kecamatan' => $this->validFilterValue($request->query('kecamatan'), Kecamatan::class), + 'desa' => $this->validFilterValue($request->query('desa'), DesaKelurahan::class), + 'komoditas' => $this->validFilterValue($request->query('komoditas'), Komoditas::class), + 'segmentasi' => $this->validSegmentasi((string) $request->query('segmentasi', 'all')), + ]; + + if ($filters['desa'] !== 'all') { + $desa = DesaKelurahan::query()->find($filters['desa']); + + if (! $desa || ($filters['kecamatan'] !== 'all' && (string) $desa->kecamatan_id !== $filters['kecamatan'])) { + $filters['desa'] = 'all'; + } + } + + if ($filters['kecamatan'] === 'all') { + $filters['desa'] = 'all'; + } + + return $filters; + } + + private function validFilterValue(mixed $value, string $modelClass): string + { + if (! $value || $value === 'all') { + return 'all'; + } + + return $modelClass::query()->whereKey($value)->exists() + ? (string) $value + : 'all'; + } + + private function validSegmentasi(string $value): string + { + return in_array($value, [ + 'Penggerak', + 'Perintis', + 'Pemerhati', + 'Pemula', + 'Belum tersegmentasi', + 'all', + ], true) ? $value : 'all'; + } + + private function kabupatenOptions(): array + { + return $this->withAllOption( + 'Semua Kabupaten', + KabupatenKota::query() + ->orderBy('nama') + ->get(['id', 'kode', 'nama']) + ->map(fn (KabupatenKota $kabupaten) => [ + 'value' => (string) $kabupaten->id, + 'label' => $this->kabupatenLabel($kabupaten), + ]) + ->values() + ->all() + ); + } + + private function kecamatanOptions(): array + { + return $this->withAllOption( + 'Semua Kecamatan', + Kecamatan::query() + ->orderBy('nama') + ->get(['id', 'nama']) + ->map(fn (Kecamatan $kecamatan) => [ + 'value' => (string) $kecamatan->id, + 'label' => $kecamatan->nama, + ]) + ->values() + ->all() + ); + } + + private function desaOptions(): array + { + return $this->withAllOption( + 'Semua Desa', + DesaKelurahan::query() + ->orderBy('nama') + ->get(['id', 'nama', 'kecamatan_id']) + ->map(fn (DesaKelurahan $desa) => [ + 'value' => (string) $desa->id, + 'label' => $desa->nama, + 'parent' => (string) $desa->kecamatan_id, + ]) + ->values() + ->all() + ); + } + + private function komoditasOptions(): array + { + return $this->withAllOption( + 'Semua Komoditas', + Komoditas::query() + ->orderBy('nama') + ->get(['id', 'nama']) + ->map(fn (Komoditas $komoditas) => [ + 'value' => (string) $komoditas->id, + 'label' => $komoditas->nama, + ]) + ->values() + ->all() + ); + } + + private function segmentasiOptions(): array + { + // Aggregate bucket counts in SQL instead of pulling every skor_kesiapan_adopsi + // value into PHP just to map/filter/unique it. + $row = SkorPetani::query()->selectRaw(" + COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 4) as penggerak, + COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 3 AND skor_kesiapan_adopsi < 4) as perintis, + COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 2 AND skor_kesiapan_adopsi < 3) as pemerhati, + COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 1 AND skor_kesiapan_adopsi < 2) as pemula, + COUNT(*) FILTER (WHERE skor_kesiapan_adopsi IS NOT NULL AND skor_kesiapan_adopsi < 1) as belum_tersegmentasi + ")->first(); + + $order = [ + 'Penggerak' => $row->penggerak, + 'Perintis' => $row->perintis, + 'Pemerhati' => $row->pemerhati, + 'Pemula' => $row->pemula, + 'Belum tersegmentasi' => $row->belum_tersegmentasi, + ]; + + $segments = collect($order) + ->filter(fn ($count) => $count > 0) + ->keys() + ->map(fn ($segment) => [ + 'value' => $segment, + 'label' => $segment, + ]) + ->values() + ->all(); + + return $this->withAllOption('Semua Segmentasi', $segments); + } + + private function totalProduksiKg(array $filters): float + { + $expr = $this->kgCaseExpr('jumlah_panen', 'satuan'); + + return (float) ($this->applyProduksiFilters(Produksi::query(), $filters) + ->whereNotNull('jumlah_panen') + ->selectRaw("SUM({$expr}) as total") + ->value('total') ?? 0); + } + + private function petaniPerKomoditasChart(array $filters): array + { + $rows = $this->applyPetaniFilters(Petani::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'petani.komoditas_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('COUNT(petani.id) as total') + ->groupBy('k.id', 'k.nama') + ->orderByDesc('total') + ->get(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(), + ]; + } + + private function luasLahanPerKecamatanChart(array $filters): array + { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id') + ->leftJoin('master_kecamatan as k', 'k.id', '=', 'd.kecamatan_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('SUM(map_lahan.luas_lahan) / 10000 as total') + ->groupBy('k.id', 'k.nama') + ->havingRaw('SUM(map_lahan.luas_lahan) > 0') + ->orderByDesc('total') + ->limit(8) + ->get() + ->filter(fn ($row) => round((float) $row->total, 2) > 0) + ->values(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(), + ]; + } + + private function kondisiTanamanChart(array $filters): array + { + $rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters) + ->whereNull('map_tanaman.deleted_at') + ->whereNotNull('map_tanaman.kondisi_tanaman') + ->selectRaw('map_tanaman.kondisi_tanaman as label') + ->selectRaw('COUNT(map_tanaman.id) as total') + ->groupBy('map_tanaman.kondisi_tanaman') + ->orderByDesc('total') + ->get(); + + if ($rows->isEmpty()) { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNull('deleted_at') + ->whereNotNull('kondisi_tanaman') + ->selectRaw('kondisi_tanaman as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy('kondisi_tanaman') + ->orderByDesc('total') + ->get(); + } + + return [ + 'labels' => $rows->pluck('label')->map(fn ($label) => $this->shortEnumLabel((string) $label))->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(), + ]; + } + + private function seranganHamaChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'serangan_hama', + fn ($label) => $this->enumLabel(SeranganHama::class, (string) $label) + ); + } + + private function penyakitTanamanChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'penyakit_tanaman', + fn ($label) => $this->enumLabel(PenyakitTanaman::class, (string) $label) + ); + } + + private function keparahanSeranganChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'keparahan_serangan', + fn ($label) => $this->keparahanSeranganLabel((string) $label) + ); + } + + private function statusKepemilikanLahanChart(array $filters): array + { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNotNull('status_kepemilikan') + ->selectRaw('status_kepemilikan as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy('status_kepemilikan') + ->orderByDesc('total') + ->get(); + + return $this->chartFromRows($rows, fn ($label) => $this->shortEnumLabel((string) $label)); + } + + private function statusHakKelolaLahanChart(array $filters): array + { + $rows = $this->applySosialEkonomiFilters(SosialEkonomi::query(), $filters) + ->whereNotNull('status_kepemilikan_lahan') + ->selectRaw('status_kepemilikan_lahan as label') + ->selectRaw('COUNT(sosial_ekonomi.id) as total') + ->groupBy('status_kepemilikan_lahan') + ->orderByDesc('total') + ->get(); + + return $this->chartFromRows( + $rows, + fn ($label) => $this->enumLabel(StatusKepemilikanLahanSosialEkonomi::class, (string) $label) + ); + } + + private function praktikPemupukanChart(array $filters): array + { + $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) + ->whereNotNull('pemberian_pupuk') + ->selectRaw('pemberian_pupuk as label') + ->selectRaw('COUNT(praktik_budidaya.id) as total') + ->groupBy('pemberian_pupuk') + ->orderBy('pemberian_pupuk') + ->get(); + + return $this->chartFromRows( + $rows, + fn ($label) => $this->pemberianPupukLabel((int) $label) + ); + } + + private function produksiPerKomoditasChart(array $filters): array + { + $expr = $this->kgCaseExpr('produksi.jumlah_panen', 'produksi.satuan'); + + $rows = $this->applyProduksiFilters(Produksi::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id') + ->whereNotNull('jumlah_panen') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw("SUM({$expr}) as total") + ->groupBy('k.id', 'k.nama') + ->get() + ->filter(fn ($row) => round((float) $row->total, 2) > 0) + ->sortByDesc('total') + ->values(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(), + ]; + } + + private function produktivitasPerKomoditasChart(array $filters): array + { + $expr = $this->kgCaseExpr('produksi.jumlah_panen', 'produksi.satuan'); + + $rows = $this->applyProduksiFilters(Produksi::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('AVG(produksi.produktivitas) FILTER (WHERE produksi.produktivitas > 0) as avg_prod') + ->selectRaw("SUM({$expr}) as total_kg") + ->selectRaw('SUM(produksi.luas_panen) as total_luas') + ->groupBy('k.id', 'k.nama') + ->get() + ->map(function ($row) { + $avgProd = (float) $row->avg_prod; + $totalLuas = (float) $row->total_luas; + $totalKg = (float) $row->total_kg; + + $value = $avgProd > 0 + ? $avgProd + : ($totalLuas > 0 ? $totalKg / $totalLuas : 0); + + return (object) ['label' => $row->label, 'total' => round($value, 2)]; + }) + ->filter(fn ($row) => $row->total > 0) + ->sortByDesc('total') + ->values(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->values()->all(), + ]; + } + + private function luasPanenPerKomoditasChart(array $filters): array + { + $rows = $this->applyProduksiFilters(Produksi::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id') + ->whereNotNull('luas_panen') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('SUM(produksi.luas_panen) as total') + ->groupBy('k.id', 'k.nama') + ->havingRaw('SUM(produksi.luas_panen) > 0') + ->orderByDesc('total') + ->get(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(), + ]; + } + + private function statusPanenChart(array $filters): array + { + $rows = $this->applyProduksiFilters(Produksi::query(), $filters) + ->whereNotNull('status_panen') + ->selectRaw("COALESCE(status_panen, 'Belum ditentukan') as label") + ->selectRaw('COUNT(produksi.id) as total') + ->groupBy('status_panen') + ->orderByDesc('total') + ->get(); + + return $this->chartFromRows($rows, fn ($label) => $this->shortEnumLabel((string) $label)); + } + + private function surveyProduktivitasByUnitChart(array $filters, string $unit): array + { + $rows = $this->applySurveyProduktivitasFilters(SurveyProduktivitas::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'survey_produktivitas.komoditas_id') + ->where('satuan', $unit) + ->whereNotNull('kuantitas') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('AVG(survey_produktivitas.kuantitas) as total') + ->groupBy('k.id', 'k.nama') + ->havingRaw('AVG(survey_produktivitas.kuantitas) > 0') + ->orderByDesc('total') + ->get(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(), + ]; + } + + private function chartFromRows($rows, callable $labelResolver): array + { + return [ + 'labels' => $rows + ->pluck('label') + ->map(fn ($label) => $labelResolver($label)) + ->values() + ->all(), + 'series' => $rows + ->pluck('total') + ->map(fn ($value) => (int) $value) + ->values() + ->all(), + ]; + } + + private function tanamanAttributeChart(array $filters, string $field, callable $labelResolver): array + { + $qualifiedField = 'map_tanaman.'.$field; + + $rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters) + ->whereNull('map_tanaman.deleted_at') + ->whereNotNull($qualifiedField) + ->selectRaw($qualifiedField.' as label') + ->selectRaw('COUNT(map_tanaman.id) as total') + ->groupBy($qualifiedField) + ->orderByDesc('total') + ->get(); + + if ($rows->isEmpty()) { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNotNull($field) + ->selectRaw($field.' as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy($field) + ->orderByDesc('total') + ->get(); + } + + return $this->chartFromRows($rows, $labelResolver); + } + + private function applyPetaniFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan'])); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereHas('skor', function ($skor) use ($filters) { + $this->applySegmentasiRange($skor, $filters['segmentasi']); + }); + }); + } + + private function applyLahanFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan'])); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereIn('petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])); + }); + } + + private function applyProduksiFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all' || $filters['desa'] !== 'all' || $filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereHas('lahan', function ($lahan) use ($filters) { + $this->applyLahanFilters($lahan, $filters); + }); + }) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])); + } + + private function applySosialEkonomiFilters($query, array $filters) + { + return $query->whereHas('petani', function ($petani) use ($filters) { + $this->applyPetaniFilters($petani, $filters); + }); + } + + private function applyPraktikBudidayaFilters($query, array $filters) + { + return $query->whereHas('petani', function ($petani) use ($filters) { + $this->applyPetaniFilters($petani, $filters); + }); + } + + private function applySurveyProduktivitasFilters($query, array $filters) + { + return $query + ->whereHas('petani', function ($petani) use ($filters) { + $this->applyPetaniFilters($petani, $filters); + }) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])); + } + + private function applyTanamanTableFilters($query, array $filters) + { + return $query + ->leftJoin('map_lahan as l', 'l.id', '=', 'map_tanaman.lahan_id') + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query + ->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'l.desa_kelurahan_id') + ->where('d.kecamatan_id', $filters['kecamatan']); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('l.desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('map_tanaman.komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->where(function ($query) use ($filters) { + $query + ->whereIn('map_tanaman.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])) + ->orWhereIn('l.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])); + }); + }); + } + + private function segmentasiPetaniIdsQuery(string $segmentasi) + { + return Petani::query() + ->select('id') + ->whereHas('skor', function ($skor) use ($segmentasi) { + $this->applySegmentasiRange($skor, $segmentasi); + }); + } + + private function applySegmentasiRange($query, string $segmentasi) + { + return match ($segmentasi) { + 'Penggerak' => $query->where('skor_kesiapan_adopsi', '>=', 4), + 'Perintis' => $query->where('skor_kesiapan_adopsi', '>=', 3)->where('skor_kesiapan_adopsi', '<', 4), + 'Pemerhati' => $query->where('skor_kesiapan_adopsi', '>=', 2)->where('skor_kesiapan_adopsi', '<', 3), + 'Pemula' => $query->where('skor_kesiapan_adopsi', '>=', 1)->where('skor_kesiapan_adopsi', '<', 2), + default => $query->where(function ($query) { + $query->whereNull('skor_kesiapan_adopsi')->orWhere('skor_kesiapan_adopsi', '<', 1); + }), + }; + } + + /** + * SQL CASE expression converting a jumlah/satuan pair to kilograms, + * so aggregation happens in the database instead of pulling every + * row of `produksi` into PHP to sum with toKilograms(). + */ + private function kgCaseExpr(string $jumlahColumn, string $satuanColumn): string + { + return "CASE LOWER({$satuanColumn}) + WHEN 'ton' THEN {$jumlahColumn} * 1000 + WHEN 'kuintal' THEN {$jumlahColumn} * 100 + WHEN 'kwintal' THEN {$jumlahColumn} * 100 + ELSE {$jumlahColumn} + END"; + } + + private function shortEnumLabel(string $value): string + { + return str($value) + ->replace('_', ' ') + ->headline() + ->toString(); + } + + private function enumLabel(string $enumClass, int|string $value): string + { + $case = $enumClass::tryFrom($value); + + if (! $case) { + return $this->shortEnumLabel((string) $value); + } + + return method_exists($case, 'label') + ? $case->label() + : $this->shortEnumLabel((string) $case->value); + } + + private function keparahanSeranganLabel(string $value): string + { + return match (KeparahanSerangan::tryFrom($value)) { + KeparahanSerangan::RINGAN => 'Ringan', + KeparahanSerangan::SEDANG => 'Sedang', + KeparahanSerangan::BERAT => 'Parah', + default => $this->shortEnumLabel($value), + }; + } + + private function pemberianPupukLabel(int $value): string + { + return match (PemberianPupuk::tryFrom($value)) { + PemberianPupuk::TIDAK_PERNAH => 'Tidak pernah', + PemberianPupuk::JENIS_DOSIS_WAKTU_TIDAK_MENENTU => 'Tidak menentu', + PemberianPupuk::JENIS_SESUAI_DOSIS_TIDAK_MENENTU => 'Jenis sesuai', + PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT => 'Cukup tepat', + PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_TEPAT => 'Tepat & rutin', + default => $this->shortEnumLabel((string) $value), + }; + } + + private function toKilograms(float $value, string $unit): float + { + return match (strtolower($unit)) { + 'ton' => $value * 1000, + 'kuintal', 'kwintal' => $value * 100, + default => $value, + }; + } + + private function withAllOption(string $label, array $options): array + { + return array_merge([ + [ + 'value' => 'all', + 'label' => $label, + ], + ], $options); + } + + private function kabupatenLabel(KabupatenKota $kabupaten): string + { + return preg_replace('/^(kabupaten|kota)\s+/i', '', $kabupaten->nama); + } + + private function segmentLabel(float $score): string + { + return match (true) { + $score >= 4.0 => 'Penggerak', + $score >= 3.0 => 'Perintis', + $score >= 2.0 => 'Pemerhati', + $score >= 1.0 => 'Pemula', + default => 'Belum tersegmentasi', + }; + } +} diff --git a/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php index 9f4a55c..bb9bfca 100644 --- a/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php +++ b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php @@ -26,30 +26,14 @@ class IndikatorPertanianController extends Controller { public function index(Request $request) { - $kabupatenOptions = $this->kabupatenOptions(); - $ngadaOption = collect($kabupatenOptions)->first( - fn ($option) => str_contains(strtolower($option['label']), 'ngada') - ); - $initialFilters = $this->selectedFilters($request, $ngadaOption['value'] ?? 'all'); - - $totalPetani = $this->applyPetaniFilters(Petani::query(), $initialFilters)->count(); - $totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $initialFilters)->sum('luas_lahan')) / 10000; - $rataRataLuasLahan = $totalPetani > 0 - ? $totalLuasLahan / $totalPetani - : 0; - $totalProduksiKg = $this->totalProduksiKg($initialFilters); - $rataRataProduksiKg = $totalPetani > 0 - ? $totalProduksiKg / $totalPetani - : 0; - $rataRataProduktivitas = $totalLuasLahan > 0 - ? $totalProduksiKg / $totalLuasLahan - : 0; + $initialFilters = $this->selectedFilters($request, $this->defaultKabupatenId()); + $payload = $this->dashboardPayload($initialFilters); $filterOptions = [ [ 'key' => 'kabupaten', 'label' => 'Kabupaten', - 'options' => $kabupatenOptions, + 'options' => $this->kabupatenOptions(), 'icon' => 'tabler-map-pin', 'readonly' => true, ], @@ -75,7 +59,55 @@ class IndikatorPertanianController extends Controller ], ]; - $summaryData = [ + return view('dashboard.indikator-pertanian', [ + 'title' => 'Indikator Pertanian', + 'initialFilters' => $initialFilters, + 'filterOptions' => $filterOptions, + ] + $payload); + } + + public function data(Request $request) + { + $filters = $this->selectedFilters($request, $this->defaultKabupatenId()); + + return response()->json([ + 'filters' => $filters, + ] + $this->dashboardPayload($filters)); + } + + private function defaultKabupatenId(): string + { + $ngadaOption = collect($this->kabupatenOptions())->first( + fn ($option) => str_contains(strtolower($option['label']), 'ngada') + ); + + return $ngadaOption['value'] ?? 'all'; + } + + private function dashboardPayload(array $filters): array + { + return [ + 'summaryData' => $this->summaryData($filters), + 'chartData' => $this->chartData($filters), + ]; + } + + private function summaryData(array $filters): array + { + $totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count(); + $totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $filters)->sum('luas_lahan')) / 10000; + $rataRataLuasLahan = $totalPetani > 0 + ? $totalLuasLahan / $totalPetani + : 0; + $totalProduksiKg = $this->totalProduksiKg($filters); + $rataRataProduksiKg = $totalPetani > 0 + ? $totalProduksiKg / $totalPetani + : 0; + $rataRataProduktivitas = $totalLuasLahan > 0 + ? $totalProduksiKg / $totalLuasLahan + : 0; + + return [ [ 'value' => number_format($totalPetani, 0, ',', '.'), 'label' => 'Petani Terdata', @@ -105,30 +137,27 @@ class IndikatorPertanianController extends Controller 'variant' => 'info', ], ]; + } - return view('dashboard.indikator-pertanian', [ - 'title' => 'Indikator Pertanian', - 'initialFilters' => $initialFilters, - 'filterOptions' => $filterOptions, - 'summaryData' => $summaryData, - 'chartData' => [ - 'petaniPerKomoditas' => $this->petaniPerKomoditasChart($initialFilters), - 'luasLahanPerKecamatan' => $this->luasLahanPerKecamatanChart($initialFilters), - 'kondisiTanaman' => $this->kondisiTanamanChart($initialFilters), - 'statusKepemilikanLahan' => $this->statusKepemilikanLahanChart($initialFilters), - 'seranganHama' => $this->seranganHamaChart($initialFilters), - 'penyakitTanaman' => $this->penyakitTanamanChart($initialFilters), - 'keparahanSerangan' => $this->keparahanSeranganChart($initialFilters), - 'statusHakKelolaLahan' => $this->statusHakKelolaLahanChart($initialFilters), - 'praktikPemupukan' => $this->praktikPemupukanChart($initialFilters), - 'produksiPerKomoditas' => $this->produksiPerKomoditasChart($initialFilters), - 'produktivitasPerKomoditas' => $this->produktivitasPerKomoditasChart($initialFilters), - 'luasPanenPerKomoditas' => $this->luasPanenPerKomoditasChart($initialFilters), - 'statusPanen' => $this->statusPanenChart($initialFilters), - 'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($initialFilters, 'ha_tahun'), - 'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($initialFilters, 'pohon_tahun'), - ], - ]); + private function chartData(array $filters): array + { + return [ + 'petaniPerKomoditas' => $this->petaniPerKomoditasChart($filters), + 'luasLahanPerKecamatan' => $this->luasLahanPerKecamatanChart($filters), + 'kondisiTanaman' => $this->kondisiTanamanChart($filters), + 'statusKepemilikanLahan' => $this->statusKepemilikanLahanChart($filters), + 'seranganHama' => $this->seranganHamaChart($filters), + 'penyakitTanaman' => $this->penyakitTanamanChart($filters), + 'keparahanSerangan' => $this->keparahanSeranganChart($filters), + 'statusHakKelolaLahan' => $this->statusHakKelolaLahanChart($filters), + 'praktikPemupukan' => $this->praktikPemupukanChart($filters), + 'produksiPerKomoditas' => $this->produksiPerKomoditasChart($filters), + 'produktivitasPerKomoditas' => $this->produktivitasPerKomoditasChart($filters), + 'luasPanenPerKomoditas' => $this->luasPanenPerKomoditasChart($filters), + 'statusPanen' => $this->statusPanenChart($filters), + 'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($filters, 'ha_tahun'), + 'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($filters, 'pohon_tahun'), + ]; } private function selectedFilters(Request $request, string $defaultKabupaten): array diff --git a/resources/views/dashboard/indikator-pertanian.blade.php b/resources/views/dashboard/indikator-pertanian.blade.php index 6046602..cc3048c 100644 --- a/resources/views/dashboard/indikator-pertanian.blade.php +++ b/resources/views/dashboard/indikator-pertanian.blade.php @@ -72,6 +72,16 @@ box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12); } + .indicator-filter-bar.is-loading .form-select, + .indicator-filter-bar.is-loading .btn { + pointer-events: none; + } + + .indicator-filter-bar.is-loading .indicator-filter-select, + .indicator-filter-bar.is-loading .btn { + opacity: .65; + } + .indicator-filter-static { align-items: center; background-color: var(--bs-white); @@ -203,7 +213,7 @@
-