defaultKabupatenId(); $initialFilters = $this->selectedFilters($request, $defaultKabupatenId); $payload = $this->dashboardPayload($initialFilters); $filterOptions = [ [ 'key' => 'kabupaten', 'label' => 'Kabupaten', 'options' => $this->kabupatenOptions(), 'icon' => 'tabler-map-pin', 'readonly' => true, ], [ 'key' => 'kecamatan', 'label' => 'Kecamatan', 'options' => $this->withAllOption('Semua Kecamatan', []), ], [ 'key' => 'desa', 'label' => 'Desa', 'options' => $this->withAllOption('Semua Desa', []), ], [ 'key' => 'komoditas', 'label' => 'Komoditas', 'options' => $this->withAllOption('Semua Komoditas', []), ], [ 'key' => 'segmentasi', 'label' => 'Segmentasi', 'options' => $this->withAllOption('Semua Segmentasi', []), ], ]; return view('dashboard.indikator-pertanian', [ 'title' => 'Indikator Pertanian', 'initialFilters' => $initialFilters, 'filterOptions' => $filterOptions, ] + $payload); } public function data(Request $request) { if ($request->query('mode') === 'filter-options') { return response()->json([ 'komoditas' => $this->komoditasOptions(), 'segmentasi' => $this->segmentasiOptions(), ]); } $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 [ 'overviewData' => $this->overviewData($filters), 'landProfileData' => $this->landProfileData($filters), 'summaryData' => $this->summaryData($filters), 'chartData' => $this->chartData($filters), ]; } private function overviewData(array $filters): array { $totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count(); $totalLahan = $this->applyLahanFilters(Lahan::query(), $filters)->count(); $totalLuasLahan = (float) $this->applyLahanFilters(Lahan::has('petani'), $filters)->sum('luas_lahan'); $jumlahDesa = $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('desa_kelurahan_id') ->distinct('desa_kelurahan_id') ->count('desa_kelurahan_id'); $jumlahKecamatan = $this->applyLahanFilters(Lahan::query(), $filters) ->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id') ->whereNotNull('d.kecamatan_id') ->distinct('d.kecamatan_id') ->count('d.kecamatan_id'); $totalProduksiKg = $this->totalProduksiKg($filters); $rataRataProduktivitas = $totalLuasLahan > 0 ? $totalProduksiKg / $totalLuasLahan : 0; $tanamanProduktif = $this->tanamanProduktifCount($filters); $batchPanen = $this->batchPanenCount($filters); return [ 'kpiCards' => [ [ 'value' => number_format($totalPetani, 0, ',', '.'), 'label' => 'Jumlah Petani', 'description' => 'Petani yang tercakup dalam filter aktif', 'icon' => 'tabler-users', 'variant' => 'primary', ], [ 'value' => number_format($totalLahan, 0, ',', '.'), 'label' => 'Jumlah Kebun/Lahan', 'description' => 'Kebun atau lahan terdata', 'icon' => 'tabler-trees', 'variant' => 'success', ], [ 'value' => number_format($totalLuasLahan, 2, ',', '.').' ha', 'label' => 'Luas Lahan', 'description' => 'Akumulasi luas lahan terdata', 'icon' => 'tabler-ruler-measure', 'variant' => 'warning', ], [ 'value' => number_format($jumlahDesa, 0, ',', '.').' desa', 'label' => 'Desa/Kecamatan', 'description' => number_format($jumlahKecamatan, 0, ',', '.').' kecamatan tercakup', 'icon' => 'tabler-map-pin', 'variant' => 'info', ], [ 'value' => number_format($totalProduksiKg, 0, ',', '.').' kg', 'label' => 'Total Produksi', 'description' => $this->productionSummaryDescription($filters), 'icon' => 'tabler-wheat', 'variant' => 'primary', ], [ 'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha', 'label' => 'Produktivitas Rata-rata', 'description' => 'Berdasarkan produksi dan luas lahan terdata', 'icon' => 'tabler-chart-bar', 'variant' => 'success', ], [ 'value' => number_format($tanamanProduktif, 0, ',', '.'), 'label' => 'Tanaman Produktif', 'description' => 'Tanaman dengan kondisi baik', 'icon' => 'tabler-plant-2', 'variant' => 'warning', ], [ 'value' => number_format($batchPanen, 0, ',', '.'), 'label' => 'Batch Panen Tercatat', 'description' => 'Catatan produksi dan survey panen', 'icon' => 'tabler-basket', 'variant' => 'info', ], ], 'commodityFocus' => $this->commodityFocusData($filters), 'map' => [ 'features' => $this->farmMapFeatures($filters), 'levels' => $this->mapLevelData($filters), ], ]; } private function landProfileData(array $filters): array { $totalTanaman = (int) $this->applyLahanFilters(Lahan::query(), $filters)->sum('jumlah_tanaman'); $rataRataUmurTanaman = (float) $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('umur_tanaman') ->avg('umur_tanaman'); $komoditasCount = (int) $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('komoditas_id') ->distinct('komoditas_id') ->count('komoditas_id'); $statusLahanCount = (int) $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('status_kepemilikan') ->distinct('status_kepemilikan') ->count('status_kepemilikan'); $tanamanPenaung = $this->shadowTreeSummary($filters); $tanamanPendamping = $this->companionPlantSummary($filters); $diversityAverage = $this->plantDiversityAverage($filters); return [ 'conditionCards' => [ [ 'value' => number_format($komoditasCount, 0, ',', '.'), 'label' => 'Luas Lahan per Komoditas', 'description' => 'Komoditas dengan luas lahan terdata', 'icon' => 'tabler-chart-treemap', 'variant' => 'primary', ], [ 'value' => number_format($totalTanaman, 0, ',', '.'), 'label' => 'Jumlah Tanaman', 'description' => 'Akumulasi jumlah tanaman pada lahan', 'icon' => 'tabler-trees', 'variant' => 'success', ], [ 'value' => number_format($rataRataUmurTanaman, 1, ',', '.').' tahun', 'label' => 'Umur Tanaman', 'description' => 'Rata-rata umur tanaman terdata', 'icon' => 'tabler-calendar-stats', 'variant' => 'warning', ], [ 'value' => number_format($statusLahanCount, 0, ',', '.'), 'label' => 'Status Lahan', 'description' => 'Jumlah kategori status kepemilikan lahan', 'icon' => 'tabler-certificate', 'variant' => 'info', ], ], 'agroecologyItems' => [ ['label' => 'Sumber Air Lahan', 'value' => $this->sourceWaterSummary($filters), 'status' => 'available'], ['label' => 'Pengelolaan Air & Irigasi', 'value' => $this->waterManagementSummary($filters), 'status' => 'available'], ['label' => 'Kebutuhan Air per Musim', 'value' => $this->seasonalWaterNeedSummary($filters), 'status' => 'available'], ['label' => 'Praktik Konservasi Tanah', 'value' => $this->soilConservationSummary($filters), 'status' => 'available'], ], 'agroforestryCards' => [ [ 'value' => number_format($tanamanPenaung['total'], 0, ',', '.'), 'label' => 'Jumlah Tanaman Penaung', 'description' => $tanamanPenaung['types'].' jenis tanaman penaung', 'icon' => 'tabler-leaf', 'variant' => 'success', ], [ 'value' => number_format($tanamanPendamping['types'], 0, ',', '.'), 'label' => 'Jenis Tanaman Pendamping', 'description' => number_format($tanamanPendamping['total'], 0, ',', '.').' entri tanaman pendamping', 'icon' => 'tabler-flower', 'variant' => 'warning', ], [ 'value' => number_format(round($diversityAverage), 0, ',', '.'), 'label' => 'Tingkat Campuran Tanaman', 'description' => 'Rata-rata kombinasi tanaman utama, pendamping, dan penaung', 'icon' => 'tabler-binary-tree', 'variant' => 'primary', ], ], ]; } private function summaryData(array $filters): array { $totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count(); // $totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $filters)->sum('luas_lahan')) / 10000; $totalLuasLahan = $this->applyLahanFilters(Lahan::has('petani'), $filters)->sum('luas_lahan'); $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', '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', ], ]; } 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'), 'luasLahanPerKomoditas' => $this->luasLahanPerKomoditasChart($filters), 'jumlahTanamanPerKomoditas' => $this->jumlahTanamanPerKomoditasChart($filters), 'umurTanaman' => $this->umurTanamanChart($filters), 'sumberAir' => $this->sumberAirChart($filters), ]; } 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 { $segments = SkorPetani::query() ->whereNotNull('skor_kesiapan_adopsi') ->pluck('skor_kesiapan_adopsi') ->map(fn ($score) => $this->segmentLabel((float) $score)) ->filter() ->unique() ->sortBy(fn ($segment) => array_search($segment, [ 'Penggerak', 'Perintis', 'Pemerhati', 'Pemula', 'Belum tersegmentasi', ], true)) ->map(fn ($segment) => [ 'value' => $segment, 'label' => $segment, ]) ->values() ->all(); return $this->withAllOption('Semua Segmentasi', $segments); } private function totalProduksiKg(array $filters): float { return $this->applyProduksiFilters(Produksi::query(), $filters) ->whereNotNull('jumlah_panen') ->get(['jumlah_panen', 'satuan']) ->sum(fn (Produksi $produksi) => $this->toKilograms( (float) $produksi->jumlah_panen, (string) $produksi->satuan )); } 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 luasLahanPerKomoditasChart(array $filters): array { $rows = $this->applyLahanFilters(Lahan::query(), $filters) ->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id') ->whereNotNull('map_lahan.luas_lahan') ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") ->selectRaw('SUM(map_lahan.luas_lahan) as total') ->groupBy('k.id', 'k.nama') ->havingRaw('SUM(map_lahan.luas_lahan) > 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 jumlahTanamanPerKomoditasChart(array $filters): array { $rows = $this->applyLahanFilters(Lahan::query(), $filters) ->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id') ->whereNotNull('map_lahan.jumlah_tanaman') ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") ->selectRaw('SUM(map_lahan.jumlah_tanaman) as total') ->groupBy('k.id', 'k.nama') ->havingRaw('SUM(map_lahan.jumlah_tanaman) > 0') ->orderByDesc('total') ->get(); return [ 'labels' => $rows->pluck('label')->values()->all(), 'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(), ]; } private function umurTanamanChart(array $filters): array { $bucketOrder = ['< 3 tahun', '3-7 tahun', '8-15 tahun', '> 15 tahun']; $rows = $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('umur_tanaman') ->pluck('umur_tanaman') ->map(function ($age) { $age = (float) $age; return match (true) { $age < 3 => '< 3 tahun', $age <= 7 => '3-7 tahun', $age <= 15 => '8-15 tahun', default => '> 15 tahun', }; }) ->countBy() ->sortBy(fn ($value, $label) => array_search($label, $bucketOrder, true)); return [ 'labels' => $rows->keys()->values()->all(), 'series' => $rows->values()->map(fn ($value) => (int) $value)->values()->all(), ]; } private function sumberAirChart(array $filters): array { $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) ->get(['sumber_air']) ->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->sumber_air)) ->filter() ->countBy() ->sortDesc(); return [ 'labels' => $rows->keys()->values()->all(), 'series' => $rows->values()->map(fn ($value) => (int) $value)->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 { $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('produksi.jumlah_panen') ->selectRaw('produksi.satuan') ->get() ->groupBy('label') ->map(fn ($items) => round($items->sum( fn (Produksi $produksi) => $this->toKilograms( (float) $produksi->jumlah_panen, (string) $produksi->satuan ) ), 2)) ->filter(fn ($total) => $total > 0) ->sortDesc(); return [ 'labels' => $rows->keys()->values()->all(), 'series' => $rows->values()->map(fn ($value) => round((float) $value, 2))->values()->all(), ]; } private function produktivitasPerKomoditasChart(array $filters): array { $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('produksi.jumlah_panen') ->selectRaw('produksi.satuan') ->selectRaw('produksi.luas_panen') ->selectRaw('produksi.produktivitas') ->get() ->groupBy('label') ->map(function ($items) { $storedAverage = $items ->pluck('produktivitas') ->filter(fn ($value) => (float) $value > 0) ->avg(); if ($storedAverage > 0) { return round((float) $storedAverage, 2); } $totalProduksiKg = $items->sum( fn (Produksi $produksi) => $this->toKilograms( (float) $produksi->jumlah_panen, (string) $produksi->satuan ) ); $totalLuasPanen = $items->sum(fn (Produksi $produksi) => (float) $produksi->luas_panen); return $totalLuasPanen > 0 ? round($totalProduksiKg / $totalLuasPanen, 2) : 0; }) ->filter(fn ($total) => $total > 0) ->sortDesc(); return [ 'labels' => $rows->keys()->values()->all(), 'series' => $rows->values()->map(fn ($value) => round((float) $value, 2))->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 tanamanProduktifCount(array $filters): int { return (int) $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters) ->whereNull('map_tanaman.deleted_at') ->where('map_tanaman.kondisi_tanaman', 'baik') ->count('map_tanaman.id'); } private function batchPanenCount(array $filters): int { $produksiCount = $this->applyProduksiFilters(Produksi::query(), $filters)->count(); $surveyPanenCount = $this->applySurveyPanenFilters(SurveyPanen::query(), $filters)->count(); return (int) $produksiCount + (int) $surveyPanenCount; } private function productionSummaryDescription(array $filters): string { $chart = $this->produksiPerKomoditasChart($filters); if (empty($chart['series'])) { return 'Belum ada produksi aktual per komoditas'; } return collect($chart['labels']) ->map(fn ($label, $index) => $label.': '.number_format((float) ($chart['series'][$index] ?? 0), 0, ',', '.').' kg') ->take(3) ->implode(' | '); } private function commodityFocusData(array $filters): array { $focusCommodities = ['Kopi', 'Kakao', 'Vanili', 'Kemiri']; $rows = $this->applyLahanFilters(Lahan::query(), $filters) ->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id') ->whereIn(DB::raw('LOWER(k.nama)'), collect($focusCommodities)->map(fn ($name) => strtolower($name))->all()) ->selectRaw('k.nama as label') ->selectRaw('COUNT(map_lahan.id) as total') ->groupBy('k.nama') ->get() ->mapWithKeys(fn ($row) => [strtolower((string) $row->label) => (int) $row->total]); return collect($focusCommodities) ->map(fn ($name) => [ 'label' => $name, 'total' => $rows[strtolower($name)] ?? 0, ]) ->values() ->all(); } private function farmMapFeatures(array $filters): array { return $this->applyLahanFilters( Lahan::query()->with(['desaKelurahan.kecamatan', 'komoditas']), $filters ) ->where(function ($query) { $query ->whereNotNull('lat') ->orWhereNotNull('long') ->orWhereNotNull('path'); }) ->limit(250) ->get() ->map(fn (Lahan $lahan) => [ 'id' => $lahan->id, 'name' => $lahan->nama, 'commodity' => $lahan->komoditas?->nama ?? 'Belum ditentukan', 'area' => (float) $lahan->luas_lahan, 'village' => $lahan->desaKelurahan?->nama, 'district' => $lahan->desaKelurahan?->kecamatan?->nama, 'lat' => is_numeric($lahan->lat) ? (float) $lahan->lat : null, 'lng' => is_numeric($lahan->long) ? (float) $lahan->long : null, 'path' => is_array($lahan->path) ? $lahan->path : null, ]) ->values() ->all(); } private function mapLevelData(array $filters): array { $lahanSummaryByDesa = $this->applyLahanFilters(Lahan::query(), $filters) ->whereNotNull('desa_kelurahan_id') ->select('desa_kelurahan_id') ->selectRaw('COUNT(*) as total_lahan') ->selectRaw('COALESCE(SUM(luas_lahan), 0) as total_luas') ->groupBy('desa_kelurahan_id') ->get(); $desaIds = $lahanSummaryByDesa->pluck('desa_kelurahan_id')->filter()->unique()->values(); if ($filters['desa'] !== 'all') { $desaIds->push($filters['desa']); } $desaList = DesaKelurahan::query() ->select('id', 'nama', 'path', 'kecamatan_id') ->when($desaIds->isNotEmpty(), fn ($query) => $query->whereIn('id', $desaIds->unique()->values())) ->with([ 'kecamatan:id,nama,path,kabupaten_kota_id', 'kecamatan.kabupatenKota:id,nama,path,provinsi_id', 'kecamatan.kabupatenKota.provinsi:id,nama,path', ]) ->get() ->keyBy('id'); $geoProvinsi = []; $geoKabupaten = []; $geoKecamatan = []; $geoDesa = []; $geoLahan = []; foreach ($lahanSummaryByDesa as $summary) { $desa = $desaList->get($summary->desa_kelurahan_id); $kec = $desa?->kecamatan; $kab = $kec?->kabupatenKota; $prov = $kab?->provinsi; $totalLahan = (int) $summary->total_lahan; $totalLuas = (float) $summary->total_luas; if ($prov && $prov->path) { $this->accumulateGeoLevel($geoProvinsi, $prov->id, null, $prov->nama, $prov->path, $totalLahan, $totalLuas); } if ($kab && $kab->path) { $this->accumulateGeoLevel($geoKabupaten, $kab->id, $prov?->id, $kab->nama, $kab->path, $totalLahan, $totalLuas); } if ($kec && $kec->path) { $this->accumulateGeoLevel($geoKecamatan, $kec->id, $kab?->id, $kec->nama, $kec->path, $totalLahan, $totalLuas); } if ($desa && $desa->path) { $this->accumulateGeoLevel($geoDesa, $desa->id, $kec?->id, $desa->nama, $desa->path, $totalLahan, $totalLuas); } } $this->ensureSelectedRegionOnMap($filters, $geoProvinsi, $geoKabupaten, $geoKecamatan, $geoDesa); $mapLahan = $this->applyLahanFilters( Lahan::query()->with([ 'profile:id,nama', 'petani:id,nama', 'komoditas:id,nama', 'tanaman:id,lahan_id,komoditas_id', 'tanaman.komoditas:id,nama', ]), $filters ) ->whereNotNull('path') ->orderByDesc('updated_at') ->limit(500) ->get(); foreach ($mapLahan as $lahan) { if (! $this->hasMapPolygon($lahan->path)) { continue; } $tanaman = $lahan->tanaman ->map(fn ($tanaman) => $tanaman->komoditas?->nama) ->filter() ->unique() ->values(); if ($tanaman->isEmpty() && $lahan->komoditas?->nama) { $tanaman = collect([$lahan->komoditas->nama]); } $geoLahan[] = [ 'id' => $lahan->id, 'parent_id' => $lahan->desa_kelurahan_id, 'polygon' => $lahan->path, 'nama_petani' => $lahan->petani?->nama ?? $lahan->profile?->nama ?? 'N/A', 'luas' => (float) $lahan->luas_lahan, 'tanaman' => $tanaman, ]; } return [ 'provinsi' => array_values($geoProvinsi), 'kabupaten' => array_values($geoKabupaten), 'kecamatan' => array_values($geoKecamatan), 'desa' => array_values($geoDesa), 'lahan' => $geoLahan, ]; } private function accumulateGeoLevel(array &$bucket, int|string $id, int|string|null $parentId, string $name, mixed $polygon, int $totalLahan, float $totalLuas): void { if (! isset($bucket[$id])) { $bucket[$id] = [ 'id' => $id, 'parent_id' => $parentId, 'nama' => $name, 'polygon' => $polygon, 'total_lahan' => 0, 'total_luas' => 0, ]; } $bucket[$id]['total_lahan'] += $totalLahan; $bucket[$id]['total_luas'] += $totalLuas; } private function ensureSelectedRegionOnMap(array $filters, array &$geoProvinsi, array &$geoKabupaten, array &$geoKecamatan, array &$geoDesa): void { $kabupaten = $filters['kabupaten'] !== 'all' ? KabupatenKota::query()->with('provinsi:id,nama,path')->find($filters['kabupaten']) : null; if ($kabupaten && $kabupaten->path && ! isset($geoKabupaten[$kabupaten->id])) { $this->accumulateGeoLevel($geoKabupaten, $kabupaten->id, $kabupaten->provinsi_id, $kabupaten->nama, $kabupaten->path, 0, 0); } if ($kabupaten?->provinsi && $kabupaten->provinsi->path && ! isset($geoProvinsi[$kabupaten->provinsi->id])) { $this->accumulateGeoLevel($geoProvinsi, $kabupaten->provinsi->id, null, $kabupaten->provinsi->nama, $kabupaten->provinsi->path, 0, 0); } if ($filters['kecamatan'] !== 'all') { $kecamatan = Kecamatan::query()->find($filters['kecamatan']); if ($kecamatan && $kecamatan->path && ! isset($geoKecamatan[$kecamatan->id])) { $this->accumulateGeoLevel($geoKecamatan, $kecamatan->id, $kecamatan->kabupaten_kota_id, $kecamatan->nama, $kecamatan->path, 0, 0); } } if ($filters['desa'] !== 'all') { $desa = DesaKelurahan::query()->find($filters['desa']); if ($desa && $desa->path && ! isset($geoDesa[$desa->id])) { $this->accumulateGeoLevel($geoDesa, $desa->id, $desa->kecamatan_id, $desa->nama, $desa->path, 0, 0); } } } private function hasMapPolygon(mixed $path): bool { if (is_array($path)) { return ! empty($path); } if (! is_string($path) || blank($path) || strtolower($path) === 'null') { return false; } $decoded = json_decode($path, true); return json_last_error() === JSON_ERROR_NONE && ! empty($decoded) && $decoded !== 'null'; } private function shadowTreeSummary(array $filters): array { $items = $this->applyLahanFilters(Lahan::query(), $filters) ->get(['tanaman_penaung']) ->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->tanaman_penaung)) ->filter() ->values(); return [ 'total' => $items->count(), 'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(), ]; } private function companionPlantSummary(array $filters): array { $items = $this->applyLahanFilters(Lahan::query(), $filters) ->get(['id', 'komoditas_lain']) ->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->komoditas_lain)) ->filter() ->values(); return [ 'total' => $items->count(), 'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(), ]; } private function plantDiversityAverage(array $filters): float { $lahan = $this->applyLahanFilters(Lahan::query(), $filters) ->get(['komoditas_id', 'komoditas_lain', 'tanaman_penaung']); if ($lahan->isEmpty()) { return 0; } return (float) $lahan->avg(function (Lahan $item) { $total = $item->komoditas_id ? 1 : 0; $total += count($this->arrayValues($item->komoditas_lain)); $total += count($this->arrayValues($item->tanaman_penaung)); return $total; }); } private function sourceWaterSummary(array $filters): string { $chart = $this->sumberAirChart($filters); if (empty($chart['labels'])) { return 'Belum ada data'; } return collect($chart['labels']) ->map(fn ($label, $index) => $label.' ('.number_format((int) ($chart['series'][$index] ?? 0), 0, ',', '.').')') ->take(2) ->implode(', '); } private function waterManagementSummary(array $filters): string { $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) ->whereNotNull('kondisi_pengelolaan_air') ->pluck('kondisi_pengelolaan_air') ->map(fn ($value) => $this->waterManagementShortLabel((int) $value)) ->countBy() ->sortDesc(); return $this->topCountSummary($rows); } private function seasonalWaterNeedSummary(array $filters): string { $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) ->whereNotNull('kebutuhan_air_permusim') ->pluck('kebutuhan_air_permusim') ->map(fn ($value) => $this->shortEnumLabel((string) $value)) ->countBy() ->sortDesc(); return $this->topCountSummary($rows); } private function soilConservationSummary(array $filters): string { $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) ->get(['kebiasaan_konservasi_tanah']) ->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->kebiasaan_konservasi_tanah)) ->filter() ->countBy() ->sortDesc(); return $this->topCountSummary($rows); } private function soilConservationScoreSummary(array $filters): string { $average = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) ->whereNotNull('nilai_konservasi_tanah') ->avg('nilai_konservasi_tanah'); if ($average === null) { return 'Belum ada data'; } return 'Rata-rata '.number_format((float) $average, 1, ',', '.'); } private function topCountSummary($rows): string { if ($rows->isEmpty()) { return 'Belum ada data'; } return $rows ->map(fn ($count, $label) => $label.' ('.number_format((int) $count, 0, ',', '.').')') ->take(2) ->implode(', '); } private function waterManagementShortLabel(int $value): string { return match (KondisiPengelolaanAir::tryFrom($value)) { KondisiPengelolaanAir::KURANG_TIDAK_ADA_UPAYA => 'Kurang, tanpa upaya', KondisiPengelolaanAir::KURANG_TIDAK_SUDAH_BERUPAYA => 'Kurang, ada upaya', KondisiPengelolaanAir::CUKUP_DARI_HUJAN => 'Cukup dari hujan', KondisiPengelolaanAir::CUKUP_IRIGASI_PINJAM => 'Cukup, irigasi pinjam', KondisiPengelolaanAir::CUKUP_IRIGASI_SENDIRI => 'Cukup, irigasi sendiri', default => 'Belum ditentukan', }; } private function arrayValues(mixed $value): array { if (is_array($value)) { return collect($value) ->flatten() ->filter(fn ($item) => filled($item)) ->map(fn ($item) => is_scalar($item) ? (string) $item : json_encode($item)) ->values() ->all(); } if (is_string($value) && filled($value) && strtolower($value) !== 'null') { $decoded = json_decode($value, true); if (json_last_error() === JSON_ERROR_NONE) { return $this->arrayValues($decoded); } return [$value]; } return []; } 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 applySurveyPanenFilters($query, array $filters) { return $query->whereHas('petani', function ($petani) use ($filters) { $this->applyPetaniFilters($petani, $filters); }); } 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); }), }; } 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', }; } }