contentData($request); return view('dashboard.produktivitas', [ 'title' => $title, 'dashboardFilters' => $request->only([ 'provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id', 'komoditas_id', 'segmen', ]), ] + $filterData + $this->productionDashboardData($request)); } public function mapInformation(Request $request) { if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) { abort(403); } $validated = $request->validate([ 'level' => ['nullable', 'in:provinsi,kabupaten,kecamatan,desa'], 'id' => ['required_with:level', 'integer'], 'provinsi_id' => ['nullable', 'integer'], 'kabupatenKota_id' => ['nullable', 'integer'], 'kecamatan_id' => ['nullable', 'integer'], 'desaKelurahan_id' => ['nullable', 'integer'], 'komoditas_id' => ['nullable', 'integer'], 'segmen' => ['nullable', 'string'], ]); $filters = [ 'provinsi_id' => null, 'kabupatenKota_id' => null, 'kecamatan_id' => null, 'desaKelurahan_id' => null, 'komoditas_id' => $validated['komoditas_id'] ?? null, 'segmen' => $validated['segmen'] ?? null, ]; $regionFilter = match ($validated['level'] ?? null) { 'provinsi' => 'provinsi_id', 'kabupaten' => 'kabupatenKota_id', 'kecamatan' => 'kecamatan_id', 'desa' => 'desaKelurahan_id', default => null, }; if ($regionFilter) { $filters[$regionFilter] = $validated['id']; } $scopedRequest = Request::create('/dashboard/produktivitas/map-information', 'GET', $filters); return response()->json($this->productionMapInformationData($scopedRequest)); } public function pasar(Request $request) { if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) { abort(403); } $title = 'Pasar Dashboard'; $filterData = app(IndeksPetaniController::class)->contentData($request); return view('dashboard.pasar', [ 'title' => $title, 'dashboardFilters' => $request->only([ 'provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id', 'komoditas_id', 'segmen', ]), ] + $filterData + $this->marketDashboardData($request)); } private function marketDashboardData(Request $request): array { $products = [ ['name' => 'Kakao', 'caption' => 'Harga Biji Kering', 'product' => 'Biji kering/Biji Kupas', 'variant' => 'success'], ['name' => 'Vanili', 'caption' => 'Harga Buah Segar', 'product' => 'Buah segar hasil petik', 'variant' => 'primary'], ['name' => 'Kopi', 'caption' => 'Harga Ceri Merah', 'product' => 'Buah segar hasil petik', 'variant' => 'warning'], ['name' => 'Kemiri', 'caption' => 'Harga Gelondong', 'product' => 'Buah segar hasil petik', 'variant' => 'info'], ]; $commodities = Komoditas::query() ->whereIn('nama', array_column($products, 'name')) ->pluck('id', 'nama'); $petaniIds = $this->filteredPetaniIdsQuery($request); $prices = DB::table('harga_jual_panen') ->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id') ->whereIn('harga_jual_panen.petani_id', $petaniIds) ->whereIn('petani.komoditas_id', $commodities->values()) ->where('harga_jual_panen.harga_panen_terakhir', '>', 0) ->whereIn('harga_jual_panen.bentuk_produk', array_unique(array_column($products, 'product'))) ->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') ->selectRaw('AVG(harga_jual_panen.harga_panen_terakhir) as average_price, COUNT(*) as records') ->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') ->get(); return [ 'priceCards' => collect($products)->map(function (array $product) use ($commodities, $prices) { $commodityId = $commodities->get($product['name']); $price = $commodityId === null ? null : $prices->first(fn ($row) => (int) $row->komoditas_id === (int) $commodityId && $row->bentuk_produk === $product['product'] ); return [ 'value' => 'Rp '.number_format((float) ($price?->average_price ?? 0), 0, ',', '.'), 'label' => $product['name'].' - '.$product['caption'], 'description' => 'Harga rata-rata panen terakhir di tingkat petani', 'records' => (int) ($price?->records ?? 0), 'unit' => '/kg', 'variant' => $product['variant'], 'icon' => 'tabler-cash', ]; })->all(), 'marketCharts' => $this->marketChannelCharts($request), 'priceTrend' => $this->marketPriceTrend($request), 'premiumAwareness' => $this->marketPremiumAwareness($request), ]; } private function marketPriceTrend(Request $request): array { $products = ['Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi (Rp/kg)']; $periods = [ 'harga_dua_tahun_lalu' => 'Dua Tahun Lalu', 'harga_tahun_lalu' => 'Tahun Lalu', 'harga_panen_terakhir' => 'Panen Terakhir', ]; $commodities = Komoditas::query() ->when($request->filled('komoditas_id'), fn ($query) => $query->where('id', $request->komoditas_id)) ->orderBy('nama') ->get(['id', 'nama']); $query = DB::table('harga_jual_panen') ->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id') ->whereIn('harga_jual_panen.petani_id', $this->filteredPetaniIdsQuery($request)) ->whereIn('petani.komoditas_id', $commodities->pluck('id')) ->whereIn('harga_jual_panen.bentuk_produk', $products) ->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk') ->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk'); foreach (array_keys($periods) as $field) { $query->selectRaw("AVG(CASE WHEN harga_jual_panen.{$field} > 0 THEN harga_jual_panen.{$field} END) as {$field}"); } $prices = $query->get()->groupBy('komoditas_id'); return [ 'periods' => array_values($periods), 'commodities' => $commodities->map(function ($commodity) use ($products, $periods, $prices) { $rows = $prices->get($commodity->id, collect())->keyBy('bentuk_produk'); return [ 'id' => $commodity->id, 'name' => $commodity->nama, 'series' => collect($products)->map(function ($product) use ($periods, $rows) { $row = $rows->get($product); return [ 'name' => $product === 'Terfermentasi (Rp/kg)' ? 'Terfermentasi' : $product, 'data' => array_map(fn ($field) => isset($row->{$field}) ? round((float) $row->{$field}, 2) : null, array_keys($periods)), ]; })->all(), ]; })->all(), ]; } private function marketChannelCharts(Request $request): array { $rows = DB::table('akses_pasar') ->whereNull('deleted_at') ->whereIn('petani_id', $this->filteredPetaniIdsQuery($request)) ->orderBy('id') ->get(['petani_id', 'tujuan_penjualan', 'sifat_hubungan_bisnis', 'frekuensi_penjualan', 'skor_pengalaman']) ->unique('petani_id'); $channels = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir', 'Lainnya']; $relationships = [ 'Bebas, bisa pindah pembeli kapan saja' => 'Bebas pindah pembeli', 'Langganan tetap, tapi tidak terikat' => 'Langganan tetap', 'Berbasis hutang, harus jual ke sana karena ada pinjaman' => 'Berbasis hutang', 'Titip jual,dibayar setelah produk terjual' => 'Titip jual', 'Ada komitmen volume, sudah ada kesepakatan jumlah' => 'Komitmen volume', 'Semi-kontrak, ada kesepakatan harga tertentu' => 'Semi-kontrak', ]; $timing = ['Langsung setelah panen', 'Ditumpuk dulu beberapa waktu', 'Saat butuh uang mendesak']; $experience = collect(SkorPengalaman::options()) ->mapWithKeys(fn ($option) => [$option['value'] => $option['label']]) ->all(); return [ 'saluranPenjualan' => $this->marketResponseChart($rows, 'tujuan_penjualan', array_combine($channels, $channels), true), 'hubunganPembeli' => $this->marketResponseChart($rows, 'sifat_hubungan_bisnis', $relationships), 'waktuPenjualan' => $this->marketResponseChart($rows, 'frekuensi_penjualan', array_combine($timing, $timing)), 'aksesSaluranLain' => $this->marketResponseChart($rows, 'skor_pengalaman', $experience), ]; } private function marketPremiumAwareness(Request $request): array { $rows = DB::table('akses_pasar') ->whereNull('deleted_at') ->whereIn('petani_id', $this->filteredPetaniIdsQuery($request)) ->orderBy('id') ->get(['petani_id', 'skor_kesadaran_premium']) ->unique('petani_id'); $options = collect(SkorKesadaranPremium::options()) ->mapWithKeys(fn ($option) => [$option['value'] => $option['label']]) ->all(); $chart = $this->marketResponseChart($rows, 'skor_kesadaran_premium', $options); $chart['fullLabels'] = [ 'Belum pernah mendengar sama sekali', 'Pernah mendengar tapi belum paham persyaratannya', 'Pernah mendengar dan sedikit memahami persyaratannya', 'Sudah sangat memahami persyaratannya, tapi belum pernah mengakses pasar premium', 'Sudah mengakses dan mendapat harga premium secara konsisten', ]; return $chart; } private function marketResponseChart($rows, string $field, array $options, bool $multiple = false): array { $counts = array_fill_keys(array_keys($options), 0); $respondents = 0; foreach ($rows as $row) { $values = $multiple ? $this->arrayValues($row->{$field}) : [$row->{$field}]; $answers = []; foreach ($values as $value) { if (is_string($value) || is_int($value)) { $value = is_string($value) ? trim($value) : $value; if (array_key_exists($value, $options)) { $answers[$value] = true; } } } if ($answers === []) { continue; } $respondents++; foreach (array_keys($answers) as $answer) { $counts[$answer]++; } } return [ 'labels' => array_values($options), 'fullLabels' => $field === 'skor_pengalaman' ? array_values($options) : array_keys($options), 'series' => array_values($counts), 'respondents' => $respondents, 'percentages' => array_map(fn ($count) => $respondents > 0 ? round($count / $respondents * 100, 1) : 0, array_values($counts)), ]; } private function productionDashboardData(Request $request): array { $lahanQuery = $this->filteredLahanQuery($request); $lahan = (clone $lahanQuery) ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id') ->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'map_lahan.komoditas_id') ->leftJoin('master_desa_kelurahan', 'master_desa_kelurahan.id', '=', 'map_lahan.desa_kelurahan_id') ->select([ 'map_lahan.*', 'profiles.nama as profile_nama', 'master_komoditas.nama as komoditas_nama', 'master_desa_kelurahan.nama as desa_nama', ]) ->get(); $lahanIds = $lahan->pluck('id')->filter()->values(); $tanaman = DB::table('map_tanaman') ->whereNull('deleted_at') ->when($lahanIds->isNotEmpty(), fn ($query) => $query->whereIn('lahan_id', $lahanIds)) ->when($lahanIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0')) ->get([ 'id', 'lahan_id', 'komoditas_id', 'kondisi_tanaman', 'umur_tanaman', 'serangan_hama', 'penyakit_tanaman', 'keparahan_serangan', 'jenis_hama', 'jenis_penyakit', ]); $conditionSource = $tanaman->whereNotNull('kondisi_tanaman')->isNotEmpty() ? $tanaman : $lahan; $conditionSourceName = $tanaman->whereNotNull('kondisi_tanaman')->isNotEmpty() ? 'tanaman' : 'lahan'; $healthSource = $this->healthSource($lahan, $tanaman); $healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan'; $landMetrics = $this->karakteristikLandMetrics($request); $totalLuasLahan = $landMetrics['total_luas_lahan']; $avgLuasPerPetani = $landMetrics['ratarata_luas_lahan']; $actualProductivity = $this->actualProductivityByCommodityChart($request); $productivityCards = $this->productivityKpiCards($actualProductivity); $praktikBudidaya = $this->filteredPraktikBudidaya($request); $surveyPanen = $this->filteredSurveyPanen($request); $aksesPasar = $this->filteredAksesPasar($request); $kpiCards = [ [ 'value' => number_format($totalLuasLahan, 2, ',', '.'), 'unit' => 'ha', 'label' => 'Total Luas Lahan', 'description' => 'Seluruh lahan petani', 'icon' => 'tabler-chart-area-line', 'variant' => 'success', ], [ 'value' => number_format($avgLuasPerPetani, 2, ',', '.'), 'unit' => 'ha', 'label' => 'Rata-rata Luas Lahan', 'description' => 'Rata-rata per petani', 'icon' => 'tabler-ruler-measure', 'variant' => 'danger', ], ]; $kpiCards = array_merge($kpiCards, $productivityCards); return [ 'kpiCards' => $kpiCards, 'mapData' => $this->productionMapData($request), 'chartData' => [ 'umurTanaman' => $this->ageBucketByCommodityChart($healthSource, $lahan, 'umur_tanaman'), 'jumlahTanamanPerKomoditas' => $this->plantCountByCommodityChart($tanaman, $lahan), 'kondisiTanaman' => $this->attributeChart($conditionSource, 'kondisi_tanaman', fn ($value) => $this->conditionLabel($value)), 'keparahanSerangan' => $this->attributeChart($healthSource, 'keparahan_serangan', fn ($value) => $this->severityLabel($value)), 'gejalaSerangan' => $this->symptomChart($healthSource), 'intensitasPemangkasan' => $this->enumOptionByCommodityChart($praktikBudidaya, 'frekuensi_pemangkasan_utama', FrekuensiPemangkasanUtama::class), 'intensitasPemupukan' => $this->enumOptionByCommodityChart($praktikBudidaya, 'pemberian_pupuk', PemberianPupuk::class), 'metodePenangananHamaPenyakit' => $this->enumOptionByCommodityChart($praktikBudidaya, 'metode_penanganan_opt', MetodePenangananHamaPenyakit::class), 'intensitasPembersihanLahan' => $this->enumOptionByCommodityChart($praktikBudidaya, 'frekuensi_sanitasi_lahan', FrekuensiSanitasiLahan::class), 'sumberAirKebun' => $this->arrayOptionByCommodityChart( $praktikBudidaya, 'sumber_air', ['Sungai/mata air', 'Sumur', 'Mengandalkan hujan saja', 'Lainnya'], fn ($value) => in_array($value, ['Sungai/mata air', 'Sumur', 'Mengandalkan hujan saja'], true) ? $value : 'Lainnya', ), 'kebiasaanPraktikBaik' => $this->arrayOptionByCommodityChart( $praktikBudidaya, 'kebiasaan_konservasi_tanah', [ 'Membuat pematang/tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah', 'Menumpuk daun kering/sisa tanaman di sekitar pohon', 'Menanam tanaman lain di sela-sela komoditas utama untuk menutupi tanah', 'Membuat lubang atau parit kecil untuk menampung air hujan', 'Tidak ada yang seperti itu di kebun saya', ], fn ($value) => $value === 'Membuat pematang atau tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah' ? 'Membuat pematang/tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah' : $value, ), 'praktikSortasiPanen' => $this->enumOptionChart($surveyPanen, 'praktik_sortasi', PraktikSortasi::class), 'praktikFermentasiPascapanen' => $this->fermentationPracticeByCommodityChart($aksesPasar), 'produktivitasAktualPerKomoditas' => $actualProductivity, ], ]; } private function productionMapData(Request $request): array { $levels = $this->productionMapLevelData($request); return [ 'filters' => [ 'provinsi' => (string) $request->input('provinsi_id', ''), 'kabupaten' => (string) $request->input('kabupatenKota_id', ''), 'kecamatan' => (string) $request->input('kecamatan_id', ''), 'desa' => (string) $request->input('desaKelurahan_id', ''), ], 'levels' => $levels, 'information' => $this->productionMapInformationData($request), ]; } private function productionMapInformationData(Request $request): array { $filteredMapLahan = (clone $this->filteredLahanQuery($request)) ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id') ->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id') ->tap(fn ($query) => $this->whereActiveMapOwner($query)); $mapLahanRows = (clone $filteredMapLahan)->get([ 'map_lahan.id', 'map_lahan.petani_id', 'map_lahan.luas_lahan', 'map_lahan.serangan_hama', 'map_lahan.penyakit_tanaman', 'map_lahan.jenis_hama', 'map_lahan.jenis_penyakit', ]); $petaniIds = $mapLahanRows->pluck('petani_id')->filter()->unique()->values(); $commodityRows = (clone $filteredMapLahan) ->leftJoin('master_komoditas as map_komoditas', 'map_komoditas.id', '=', 'map_lahan.komoditas_id') ->whereNotNull('map_lahan.petani_id') ->selectRaw("COALESCE(map_komoditas.nama, 'Belum ditentukan') as nama") ->selectRaw('COUNT(DISTINCT map_lahan.petani_id) as jumlah') ->groupBy('map_komoditas.nama') ->orderByDesc('jumlah') ->get() ->map(fn ($row) => ['nama' => $row->nama, 'jumlah' => (int) $row->jumlah]) ->all(); $tanamanByLahan = DB::table('map_tanaman') ->whereNull('map_tanaman.deleted_at') ->whereIn('map_tanaman.lahan_id', $mapLahanRows->pluck('id')) ->get([ 'map_tanaman.lahan_id', 'map_tanaman.serangan_hama', 'map_tanaman.penyakit_tanaman', 'map_tanaman.jenis_hama', 'map_tanaman.jenis_penyakit', ]) ->groupBy('lahan_id'); $optCounts = []; foreach ($mapLahanRows as $land) { foreach ($this->productionOptDetails($land, $tanamanByLahan->get($land->id, collect())) as $opt) { $optCounts[$opt] = ($optCounts[$opt] ?? 0) + 1; } } arsort($optCounts); $mainCommodityIds = DB::table('master_komoditas') ->where('is_utama', true) ->whereNull('deleted_at') ->select('id'); $scorePetaniIds = DB::table('petani') ->whereIn('petani.id', $petaniIds) ->whereIn('petani.komoditas_id', $mainCommodityIds) ->select('petani.id') ->pluck('petani.id'); $scoreQuery = DB::table('petani') ->leftJoin('petani_skor', function ($join) { $join->on('petani_skor.petani_id', '=', 'petani.id') ->whereNull('petani_skor.deleted_at'); }) ->whereNull('petani.deleted_at') ->whereIn('petani.id', $scorePetaniIds); $averageIndex = (clone $scoreQuery)->avg('petani_skor.skor_kesiapan_adopsi'); $segmentCase = "CASE WHEN petani_skor.skor_kesiapan_adopsi >= 4 THEN 'Penggerak' WHEN petani_skor.skor_kesiapan_adopsi >= 3 THEN 'Perintis' WHEN petani_skor.skor_kesiapan_adopsi >= 2 THEN 'Pemerhati' WHEN petani_skor.skor_kesiapan_adopsi >= 1 THEN 'Pemula' ELSE 'Belum Lengkap' END"; $segmentCounts = (clone $scoreQuery) ->selectRaw("{$segmentCase} as segmen") ->selectRaw('COUNT(DISTINCT petani.id) as jumlah') ->groupByRaw($segmentCase) ->get(); $segmentPriority = [ 'Penggerak' => 0, 'Perintis' => 1, 'Pemerhati' => 2, 'Pemula' => 3, 'Belum Lengkap' => 4, ]; $dominantSegment = $segmentCounts ->sort(function ($left, $right) use ($segmentPriority) { return ((int) $right->jumlah <=> (int) $left->jumlah) ?: (($segmentPriority[$left->segmen] ?? 99) <=> ($segmentPriority[$right->segmen] ?? 99)); }) ->first(); $averageSegment = match (true) { $averageIndex === null => 'Belum Lengkap', $averageIndex >= 4 => 'Penggerak', $averageIndex >= 3 => 'Perintis', $averageIndex >= 2 => 'Pemerhati', $averageIndex >= 1 => 'Pemula', default => 'Belum Lengkap', }; return [ 'jumlah_petani' => $petaniIds->count(), 'jumlah_lahan' => $mapLahanRows->count(), 'total_luas_lahan' => (float) $mapLahanRows->sum('luas_lahan'), 'petani_per_komoditas' => $commodityRows, 'opt' => collect($optCounts) ->map(fn ($jumlah, $nama) => ['nama' => $nama, 'jumlah' => $jumlah]) ->values() ->all(), 'indeks' => $averageIndex === null ? null : round((float) $averageIndex, 2), 'segmen' => $averageSegment, 'segmen_dominan' => $dominantSegment->segmen ?? 'Belum Lengkap', ]; } private function productionMapLevelData(Request $request): array { $lahanSummaryByDesa = (clone $this->filteredLahanQuery($request)) ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id') ->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id') ->tap(fn ($query) => $this->whereActiveMapOwner($query)) ->whereNotNull('map_lahan.desa_kelurahan_id') ->select('map_lahan.desa_kelurahan_id') ->selectRaw('COUNT(map_lahan.id) as total_lahan') ->selectRaw('COALESCE(SUM(map_lahan.luas_lahan), 0) as total_luas') ->groupBy('map_lahan.desa_kelurahan_id') ->get(); $desaIds = $lahanSummaryByDesa->pluck('desa_kelurahan_id')->filter()->unique()->values(); if ($request->filled('desaKelurahan_id')) { $desaIds->push($request->desaKelurahan_id); } $desaList = DesaKelurahan::query() ->select('id', 'nama', 'path', 'kecamatan_id') ->when( $desaIds->isNotEmpty(), fn ($query) => $query->whereIn('id', $desaIds->unique()->values()), fn ($query) => $query->whereRaw('1 = 0') ) ->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 = []; foreach ($lahanSummaryByDesa as $summary) { $desa = $desaList->get($summary->desa_kelurahan_id); $kecamatan = $desa?->kecamatan; $kabupaten = $kecamatan?->kabupatenKota; $provinsi = $kabupaten?->provinsi; $totalLahan = (int) $summary->total_lahan; $totalLuas = (float) $summary->total_luas; if ($provinsi && $provinsi->path) { $this->accumulateProductionGeoLevel($geoProvinsi, $provinsi->id, null, $provinsi->nama, $provinsi->path, $totalLahan, $totalLuas); } if ($kabupaten && $kabupaten->path) { $this->accumulateProductionGeoLevel($geoKabupaten, $kabupaten->id, $provinsi?->id, $kabupaten->nama, $kabupaten->path, $totalLahan, $totalLuas); } if ($kecamatan && $kecamatan->path) { $this->accumulateProductionGeoLevel($geoKecamatan, $kecamatan->id, $kabupaten?->id, $kecamatan->nama, $kecamatan->path, $totalLahan, $totalLuas); } if ($desa && $desa->path) { $this->accumulateProductionGeoLevel($geoDesa, $desa->id, $kecamatan?->id, $desa->nama, $desa->path, $totalLahan, $totalLuas); } } $this->ensureSelectedProductionRegionOnMap($request, $geoProvinsi, $geoKabupaten, $geoKecamatan, $geoDesa); $mapLahan = (clone $this->filteredLahanQuery($request)) ->whereNotNull('map_lahan.path') ->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id') ->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id') ->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'map_lahan.komoditas_id') ->tap(fn ($query) => $this->whereActiveMapOwner($query)) ->orderByDesc('map_lahan.updated_at') ->limit(500) ->get([ 'map_lahan.id', 'map_lahan.desa_kelurahan_id', 'map_lahan.path', 'map_lahan.luas_lahan', 'map_lahan.jumlah_tanaman', 'map_lahan.petani_id', 'map_lahan.umur_tanaman', 'map_lahan.serangan_hama', 'map_lahan.penyakit_tanaman', 'map_lahan.jenis_hama', 'map_lahan.jenis_penyakit', 'profiles.nama as profile_nama', 'petani.nama as petani_nama', 'master_komoditas.nama as komoditas_nama', ]); $lahanIds = $mapLahan->pluck('id')->filter()->values(); $tanamanByLahan = DB::table('map_tanaman') ->whereNull('map_tanaman.deleted_at') ->when($lahanIds->isNotEmpty(), fn ($query) => $query->whereIn('map_tanaman.lahan_id', $lahanIds)) ->when($lahanIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0')) ->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'map_tanaman.komoditas_id') ->get([ 'map_tanaman.lahan_id', 'map_tanaman.umur_tanaman', 'map_tanaman.serangan_hama', 'map_tanaman.penyakit_tanaman', 'map_tanaman.jenis_hama', 'map_tanaman.jenis_penyakit', 'master_komoditas.nama as komoditas_nama', ]) ->groupBy('lahan_id'); $petaniIds = $mapLahan->pluck('petani_id')->filter()->unique()->values(); $practiceByPetani = DB::table('praktik_budidaya') ->whereNull('praktik_budidaya.deleted_at') ->whereIn('praktik_budidaya.petani_id', $petaniIds) ->orderByDesc('praktik_budidaya.updated_at') ->get([ 'praktik_budidaya.petani_id', 'praktik_budidaya.frekuensi_pemangkasan_utama', 'praktik_budidaya.pemberian_pupuk', 'praktik_budidaya.kebiasaan_konservasi_tanah', ]) ->groupBy('petani_id') ->map(fn ($items) => $items->first()); $geoLahan = $mapLahan ->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path)) ->map(function ($lahan) use ($tanamanByLahan, $practiceByPetani) { $tanamanRows = $tanamanByLahan->get($lahan->id, collect()); $komoditas = $this->productionCommodityDetails($lahan, $tanamanRows); $opt = $this->productionOptDetails($lahan, $tanamanRows); $practice = $practiceByPetani->get($lahan->petani_id); return [ '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, 'komoditas' => $komoditas, 'opt' => $opt, 'pemangkasan' => $this->enumLabel(FrekuensiPemangkasanUtama::class, $practice->frekuensi_pemangkasan_utama ?? null, 'Belum ada data'), 'pemupukan' => $this->enumLabel(PemberianPupuk::class, $practice->pemberian_pupuk ?? null, 'Belum ada data'), 'praktik_baik' => $this->arrayValues($practice->kebiasaan_konservasi_tanah ?? null), ]; }) ->values() ->all(); return [ 'provinsi' => array_values($geoProvinsi), 'kabupaten' => array_values($geoKabupaten), 'kecamatan' => array_values($geoKecamatan), 'desa' => array_values($geoDesa), 'lahan' => $geoLahan, ]; } private function whereActiveMapOwner($query) { return $query->where(function ($query) { $query ->where(function ($query) { $query ->whereNotNull('map_lahan.petani_id') ->whereNotNull('petani.id') ->whereNull('petani.deleted_at'); }) ->orWhere(function ($query) { $query ->whereNull('map_lahan.petani_id') ->whereNotNull('map_lahan.profile_id') ->whereNotNull('profiles.id') ->whereNull('profiles.deleted_at'); }); }); } private function productionCommodityDetails($lahan, $tanamanRows): array { $details = $tanamanRows ->groupBy(fn ($row) => $row->komoditas_nama ?: ($lahan->komoditas_nama ?: 'Belum ditentukan')) ->map(function ($rows, string $name) { $ages = $rows ->pluck('umur_tanaman') ->filter(fn ($age) => is_numeric($age)) ->map(fn ($age) => (float) $age); return [ 'nama' => $name, 'jumlah' => $rows->count(), 'umur' => $ages->isNotEmpty() ? round((float) $ages->avg(), 1) : null, ]; }) ->values(); if ($details->isEmpty()) { $details->push([ 'nama' => $lahan->komoditas_nama ?: 'Belum ditentukan', 'jumlah' => (int) ($lahan->jumlah_tanaman ?? 0), 'umur' => is_numeric($lahan->umur_tanaman) ? round((float) $lahan->umur_tanaman, 1) : null, ]); } return $details->all(); } private function productionOptDetails($lahan, $tanamanRows): array { return $tanamanRows ->flatMap(fn ($row) => $this->optValuesFromRow($row)) ->merge($this->optValuesFromRow($lahan)) ->filter() ->unique(fn ($item) => strtolower((string) $item)) ->values() ->all(); } private function optValuesFromRow($row): array { $hamaDetails = collect($this->arrayValues($row->jenis_hama ?? null))->filter(); $penyakitDetails = collect($this->arrayValues($row->jenis_penyakit ?? null))->filter(); return collect() ->merge($hamaDetails->isNotEmpty() ? $hamaDetails : [$this->enumLabel(SeranganHama::class, $row->serangan_hama ?? null)]) ->merge($penyakitDetails->isNotEmpty() ? $penyakitDetails : [$this->enumLabel(PenyakitTanaman::class, $row->penyakit_tanaman ?? null)]) ->filter(fn ($value) => $value !== null && $value !== '') ->map(fn ($value) => str((string) $value)->replace('_', ' ')->title()->toString()) ->values() ->all(); } private function accumulateProductionGeoLevel(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 ensureSelectedProductionRegionOnMap(Request $request, array &$geoProvinsi, array &$geoKabupaten, array &$geoKecamatan, array &$geoDesa): void { if ($request->filled('provinsi_id')) { $provinsi = Provinsi::query()->find($request->provinsi_id); if ($provinsi && $provinsi->path && ! isset($geoProvinsi[$provinsi->id])) { $this->accumulateProductionGeoLevel($geoProvinsi, $provinsi->id, null, $provinsi->nama, $provinsi->path, 0, 0); } } if ($request->filled('kabupatenKota_id')) { $kabupaten = KabupatenKota::query()->with('provinsi:id,nama,path')->find($request->kabupatenKota_id); if ($kabupaten && $kabupaten->path && ! isset($geoKabupaten[$kabupaten->id])) { $this->accumulateProductionGeoLevel($geoKabupaten, $kabupaten->id, $kabupaten->provinsi_id, $kabupaten->nama, $kabupaten->path, 0, 0); } if ($kabupaten?->provinsi && $kabupaten->provinsi->path && ! isset($geoProvinsi[$kabupaten->provinsi->id])) { $this->accumulateProductionGeoLevel($geoProvinsi, $kabupaten->provinsi->id, null, $kabupaten->provinsi->nama, $kabupaten->provinsi->path, 0, 0); } } if ($request->filled('kecamatan_id')) { $kecamatan = Kecamatan::query()->find($request->kecamatan_id); if ($kecamatan && $kecamatan->path && ! isset($geoKecamatan[$kecamatan->id])) { $this->accumulateProductionGeoLevel($geoKecamatan, $kecamatan->id, $kecamatan->kabupaten_kota_id, $kecamatan->nama, $kecamatan->path, 0, 0); } } if ($request->filled('desaKelurahan_id')) { $desa = DesaKelurahan::query()->find($request->desaKelurahan_id); if ($desa && $desa->path && ! isset($geoDesa[$desa->id])) { $this->accumulateProductionGeoLevel($geoDesa, $desa->id, $desa->kecamatan_id, $desa->nama, $desa->path, 0, 0); } } } private function hasMapPolygon(mixed $path): bool { return ! empty($this->normalizeMapPolygon($path)); } private function normalizeMapPolygon(mixed $path): mixed { if (is_array($path)) { return $path; } if (! is_string($path) || blank($path) || strtolower($path) === 'null') { return null; } $decoded = $path; for ($i = 0; $i < 3 && is_string($decoded); $i++) { $decoded = json_decode($decoded, true); if (json_last_error() !== JSON_ERROR_NONE) { return null; } } return $decoded === 'null' ? null : $decoded; } private function filteredPraktikBudidaya(Request $request) { $petaniIds = $this->filteredPetaniIdsQuery($request); return DB::table('praktik_budidaya') ->whereNull('praktik_budidaya.deleted_at') ->whereIn('praktik_budidaya.petani_id', $petaniIds) ->leftJoin('petani as praktik_petani', 'praktik_petani.id', '=', 'praktik_budidaya.petani_id') ->leftJoin('master_komoditas as praktik_komoditas', 'praktik_komoditas.id', '=', 'praktik_petani.komoditas_id') ->get([ 'praktik_budidaya.frekuensi_pemangkasan_utama', 'praktik_budidaya.pemberian_pupuk', 'praktik_budidaya.metode_penanganan_opt', 'praktik_budidaya.frekuensi_sanitasi_lahan', 'praktik_budidaya.sumber_air', 'praktik_budidaya.kebiasaan_konservasi_tanah', 'praktik_komoditas.nama as komoditas_nama', ]); } private function filteredSurveyPanen(Request $request) { $petaniIds = $this->filteredPetaniIdsQuery($request); return DB::table('survey_panen') ->whereNull('survey_panen.deleted_at') ->whereIn('survey_panen.petani_id', $petaniIds) ->get(['praktik_sortasi']); } private function filteredAksesPasar(Request $request) { $petaniIds = $this->filteredPetaniIdsQuery($request); return DB::table('akses_pasar') ->whereNull('akses_pasar.deleted_at') ->whereIn('akses_pasar.petani_id', $petaniIds) ->leftJoin('petani as akses_petani', 'akses_petani.id', '=', 'akses_pasar.petani_id') ->leftJoin('master_komoditas as akses_komoditas', 'akses_komoditas.id', '=', 'akses_petani.komoditas_id') ->get([ 'akses_pasar.praktik_fermentasi', 'akses_komoditas.nama as komoditas_nama', ]); } private function filteredSurveyProduktivitas(Request $request) { $petaniIds = $this->filteredPetaniIdsQuery($request); return DB::table('survey_produktivitas') ->whereNull('survey_produktivitas.deleted_at') ->whereIn('survey_produktivitas.petani_id', $petaniIds) ->when($request->filled('komoditas_id'), fn ($query) => $query->where('survey_produktivitas.komoditas_id', $request->komoditas_id)) ->leftJoin('master_komoditas', 'master_komoditas.id', '=', 'survey_produktivitas.komoditas_id') ->get([ 'survey_produktivitas.komoditas_id', 'survey_produktivitas.kuantitas', 'survey_produktivitas.satuan', 'master_komoditas.nama as komoditas_nama', ]); } private function plantCountByCommodityChart($tanaman, $lahan): array { $lahanKomoditas = $lahan ->mapWithKeys(fn ($row) => [(int) $row->id => $row->komoditas_nama ?: 'Belum ditentukan']); $komoditasNames = DB::table('master_komoditas') ->whereNull('deleted_at') ->pluck('nama', 'id'); $counts = $tanaman ->map(function ($row) use ($komoditasNames, $lahanKomoditas) { if ($row->komoditas_id && $komoditasNames->has($row->komoditas_id)) { return $komoditasNames[$row->komoditas_id]; } return $lahanKomoditas[(int) $row->lahan_id] ?? 'Belum ditentukan'; }) ->filter() ->countBy() ->sortDesc(); return [ 'labels' => $counts->keys()->values()->all(), 'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(), ]; } private function productivityKpiCards(array $actualProductivity): array { $variants = ['primary', 'warning', 'info', 'secondary']; $icon = 'tabler-chart-bar'; return collect($actualProductivity['labels']) ->map(function (string $name, int $index) use ($actualProductivity, $variants, $icon) { return [ 'value' => number_format((float) ($actualProductivity['series'][$index] ?? 0), 2, ',', '.'), 'unit' => 'kg/ha', 'label' => 'Produktivitas Aktual '.$name, 'description' => 'Hasil panen per musim dibagi total luas lahan', 'icon' => $icon, 'variant' => $variants[$index % count($variants)], ]; }) ->all(); } private function actualProductivityByCommodityChart(Request $request): array { $preferredOrder = ['Kakao', 'Vanili', 'Kemiri', 'Kopi']; $selectedCommodities = Komoditas::query() ->whereNull('deleted_at') ->whereIn('nama', $preferredOrder) ->get(['id', 'nama']) ->sortBy(fn ($item) => array_search($item->nama, $preferredOrder, true)) ->values(); $petaniIds = $this->filteredPetaniIdsQuery($request); // Aggregate separately so multiple plots do not duplicate a farmer's seasonal harvest. $landAreaByCommodity = DB::table('map_lahan') ->join('petani', 'petani.id', '=', 'map_lahan.petani_id') ->whereNull('map_lahan.deleted_at') ->whereIn('map_lahan.petani_id', clone $petaniIds) ->select('petani.komoditas_id') ->selectRaw('SUM(map_lahan.luas_lahan) as total_luas') ->groupBy('petani.komoditas_id') ->pluck('total_luas', 'komoditas_id'); $productionByCommodity = DB::table('survey_panen') ->join('petani', 'petani.id', '=', 'survey_panen.petani_id') ->whereNull('survey_panen.deleted_at') ->whereIn('survey_panen.petani_id', clone $petaniIds) ->select('petani.komoditas_id') ->selectRaw('SUM(survey_panen.hasil_komoditas_utama_permusim) as total_produksi_kg') ->groupBy('petani.komoditas_id') ->pluck('total_produksi_kg', 'komoditas_id'); return [ 'labels' => $selectedCommodities->pluck('nama')->values()->all(), 'series' => $selectedCommodities ->map(function ($item) use ($landAreaByCommodity, $productionByCommodity) { $commodityId = (int) $item->id; $totalLandArea = (float) ($landAreaByCommodity[$commodityId] ?? 0); $totalProductionKg = (float) ($productionByCommodity[$commodityId] ?? 0); return $totalLandArea > 0 ? round($totalProductionKg / $totalLandArea, 2) : 0; }) ->values() ->all(), ]; } private function filteredLahanQuery(Request $request) { return DB::table('map_lahan') ->whereNull('map_lahan.deleted_at') ->leftJoin('master_desa_kelurahan as desa_filter', 'map_lahan.desa_kelurahan_id', '=', 'desa_filter.id') ->leftJoin('master_kecamatan as kecamatan_filter', 'desa_filter.kecamatan_id', '=', 'kecamatan_filter.id') ->leftJoin('master_kabupaten_kota as kabupaten_filter', 'kecamatan_filter.kabupaten_kota_id', '=', 'kabupaten_filter.id') ->when($request->filled('provinsi_id'), fn ($query) => $query->where('kabupaten_filter.provinsi_id', $request->provinsi_id)) ->when($request->filled('kabupatenKota_id'), fn ($query) => $query->where('kecamatan_filter.kabupaten_kota_id', $request->kabupatenKota_id)) ->when($request->filled('kecamatan_id'), fn ($query) => $query->where('desa_filter.kecamatan_id', $request->kecamatan_id)) ->when($request->filled('desaKelurahan_id'), fn ($query) => $query->where('map_lahan.desa_kelurahan_id', $request->desaKelurahan_id)) ->when($request->filled('komoditas_id'), fn ($query) => $query->where('map_lahan.komoditas_id', $request->komoditas_id)) ->when($request->filled('segmen'), function ($query) use ($request) { $query->whereIn('map_lahan.petani_id', $this->petaniIdsBySegment($request->segmen)); }); } private function karakteristikLandMetrics(Request $request): array { $filteredPetaniIds = $this->filteredPetaniIdsQuery($request); $totalPetani = (int) DB::table('petani') ->whereNull('petani.deleted_at') ->whereIn('petani.id', clone $filteredPetaniIds) ->count(); $totalLuasLahan = (float) DB::table('map_lahan') ->whereNull('map_lahan.deleted_at') ->whereIn('map_lahan.petani_id', clone $filteredPetaniIds) ->sum('map_lahan.luas_lahan'); return [ 'total_petani' => $totalPetani, 'total_luas_lahan' => $totalLuasLahan, 'ratarata_luas_lahan' => $totalLuasLahan && $totalPetani ? $totalLuasLahan / $totalPetani : 0, ]; } private function filteredPetaniIdsQuery(Request $request) { $query = DB::table('petani') ->whereNull('petani.deleted_at') ->leftJoin('master_desa_kelurahan as desa', 'petani.desa_kelurahan_id', '=', 'desa.id') ->leftJoin('master_kecamatan as kecamatan', 'desa.kecamatan_id', '=', 'kecamatan.id') ->leftJoin('master_kabupaten_kota as kabupaten', 'kecamatan.kabupaten_kota_id', '=', 'kabupaten.id') ->when($request->filled('provinsi_id'), fn ($query) => $query->where('kabupaten.provinsi_id', $request->provinsi_id)) ->when($request->filled('kabupatenKota_id'), fn ($query) => $query->where('kecamatan.kabupaten_kota_id', $request->kabupatenKota_id)) ->when($request->filled('kecamatan_id'), fn ($query) => $query->where('desa.kecamatan_id', $request->kecamatan_id)) ->when($request->filled('desaKelurahan_id'), fn ($query) => $query->where('petani.desa_kelurahan_id', $request->desaKelurahan_id)) ->when($request->filled('komoditas_id'), fn ($query) => $query->where('petani.komoditas_id', $request->komoditas_id)); if ($request->filled('segmen')) { $query->whereExists(function ($subQuery) use ($request) { $subQuery->from('petani_skor') ->whereNull('petani_skor.deleted_at') ->whereColumn('petani_skor.petani_id', 'petani.id'); match ($request->segmen) { 'Penggerak' => $subQuery->where('skor_kesiapan_adopsi', '>=', 4), 'Perintis' => $subQuery->where('skor_kesiapan_adopsi', '>=', 3) ->where('skor_kesiapan_adopsi', '<', 4), 'Pemerhati' => $subQuery->where('skor_kesiapan_adopsi', '>=', 2) ->where('skor_kesiapan_adopsi', '<', 3), 'Pemula' => $subQuery->where('skor_kesiapan_adopsi', '>=', 1) ->where('skor_kesiapan_adopsi', '<', 2), 'Belum Lengkap' => $subQuery->where(function ($query) { $query->whereNull('skor_kesiapan_adopsi') ->orWhere('skor_kesiapan_adopsi', '<', 1); }), default => null, }; }); } return $query->select('petani.id'); } private function petaniIdsBySegment(string $segment) { $query = DB::table('petani_skor') ->whereNull('deleted_at') ->select('petani_id'); match ($segment) { '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), 'Belum Lengkap' => $query->where(fn ($subQuery) => $subQuery ->whereNull('skor_kesiapan_adopsi') ->orWhere('skor_kesiapan_adopsi', '<', 1)), default => $query->whereRaw('1 = 1'), }; return $query; } private function healthSource($lahan, $tanaman) { return $tanaman->isNotEmpty() ? $tanaman : $lahan; } private function ageBucketChart($rows, string $field): array { $order = ['< 3 tahun', '3-7 tahun', '8-15 tahun', '> 15 tahun']; $counts = $rows ->pluck($field) ->filter(fn ($age) => $age !== null) ->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(); return [ 'labels' => $order, 'series' => collect($order)->map(fn ($label) => (int) ($counts[$label] ?? 0))->all(), ]; } private function ageBucketByCommodityChart($rows, $lahan, string $field): array { $order = ['< 3 tahun', '3-7 tahun', '8-15 tahun', '> 15 tahun']; $lahanKomoditas = $lahan ->mapWithKeys(fn ($row) => [(int) $row->id => $row->komoditas_nama ?: 'Belum ditentukan']); $komoditasNames = DB::table('master_komoditas') ->whereNull('deleted_at') ->pluck('nama', 'id'); $matrix = []; foreach ($rows as $row) { $age = $row->{$field} ?? null; if ($age === null || $age === '') { continue; } $commodity = 'Belum ditentukan'; if (! empty($row->komoditas_id) && $komoditasNames->has($row->komoditas_id)) { $commodity = $komoditasNames[$row->komoditas_id]; } elseif (! empty($row->lahan_id) && $lahanKomoditas->has((int) $row->lahan_id)) { $commodity = $lahanKomoditas[(int) $row->lahan_id]; } elseif (! empty($row->komoditas_nama)) { $commodity = $row->komoditas_nama; } $bucket = $this->ageBucketLabel((float) $age); $matrix[$commodity] ??= array_fill_keys($order, 0); $matrix[$commodity][$bucket]++; } ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE); return [ 'labels' => array_keys($matrix), 'series' => collect($order) ->map(fn ($bucket) => [ 'name' => $bucket, 'data' => collect($matrix)->map(fn ($counts) => (int) ($counts[$bucket] ?? 0))->values()->all(), ]) ->values() ->all(), ]; } private function ageBucketLabel(float $age): string { return match (true) { $age < 3 => '< 3 tahun', $age <= 7 => '3-7 tahun', $age <= 15 => '8-15 tahun', default => '> 15 tahun', }; } private function attributeChart($rows, string $field, callable $labeler): array { $counts = $rows ->pluck($field) ->filter(fn ($value) => $this->enumValue($value) !== null && $this->enumValue($value) !== '') ->map(fn ($value) => $labeler($value)) ->countBy() ->sortDesc(); return [ 'labels' => $counts->keys()->values()->all(), 'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(), ]; } private function enumOptionChart($rows, string $field, string $enumClass): array { $options = collect($enumClass::options()) ->mapWithKeys(fn ($option) => [(string) $option['value'] => $option['label']]); $counts = $rows ->pluck($field) ->filter(fn ($value) => $value !== null && $value !== '') ->map(fn ($value) => (string) $value) ->countBy(); return [ 'labels' => $options->values()->all(), 'series' => $options->keys()->map(fn ($value) => (int) ($counts[$value] ?? 0))->values()->all(), ]; } private function enumOptionByCommodityChart($rows, string $field, string $enumClass): array { $options = collect($enumClass::options()) ->mapWithKeys(fn ($option) => [(string) $option['value'] => $option['label']]); $matrix = []; foreach ($rows as $row) { $value = $row->{$field} ?? null; if ($value === null || $value === '') { continue; } $commodity = $row->komoditas_nama ?: 'Belum ditentukan'; $matrix[$commodity] ??= $options->keys() ->mapWithKeys(fn ($optionValue) => [(string) $optionValue => 0]) ->all(); $matrix[$commodity][(string) $value] = (int) ($matrix[$commodity][(string) $value] ?? 0) + 1; } ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE); return [ 'labels' => array_keys($matrix), 'series' => $options ->map(fn ($label, $value) => [ 'name' => $label, 'data' => collect($matrix)->map(fn ($counts) => (int) ($counts[(string) $value] ?? 0))->values()->all(), ]) ->values() ->all(), ]; } private function arrayOptionChart($rows, string $field, array $labels, ?callable $normalizer = null): array { $counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]); foreach ($rows as $row) { foreach ($this->arrayValues($row->{$field} ?? null) as $value) { $label = $normalizer ? $normalizer($value) : $value; if ($label === null || $label === '') { continue; } if (!$counts->has($label)) { $counts[$label] = 0; } $counts[$label] = (int) $counts[$label] + 1; } } return [ 'labels' => $counts->keys()->values()->all(), 'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(), ]; } private function arrayOptionByCommodityChart($rows, string $field, array $labels, ?callable $normalizer = null): array { $matrix = []; foreach ($rows as $row) { $commodity = $row->komoditas_nama ?: 'Belum ditentukan'; foreach ($this->arrayValues($row->{$field} ?? null) as $value) { $label = $normalizer ? $normalizer($value) : $value; if ($label === null || $label === '') { continue; } if (! in_array($label, $labels, true)) { $labels[] = $label; } $matrix[$commodity] ??= collect($labels) ->mapWithKeys(fn ($optionLabel) => [$optionLabel => 0]) ->all(); $matrix[$commodity][$label] = (int) ($matrix[$commodity][$label] ?? 0) + 1; } } ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE); return [ 'labels' => array_keys($matrix), 'series' => collect($labels) ->map(fn ($label) => [ 'name' => $label, 'data' => collect($matrix)->map(fn ($counts) => (int) ($counts[$label] ?? 0))->values()->all(), ]) ->values() ->all(), ]; } private function fermentationPracticeByCommodityChart($rows): array { $labels = ['Ya, rutin', 'Pernah mencoba', 'Belum pernah']; $matrix = []; foreach ($rows as $row) { $label = $this->fermentationPracticeLabel($row->praktik_fermentasi ?? null); if ($label === null) { continue; } $commodity = $row->komoditas_nama ?: 'Belum ditentukan'; $matrix[$commodity] ??= array_fill_keys($labels, 0); $matrix[$commodity][$label]++; } ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE); return [ 'labels' => array_keys($matrix), 'series' => collect($labels) ->map(fn ($label) => [ 'name' => $label, 'data' => collect($matrix)->map(fn ($counts) => (int) $counts[$label])->values()->all(), ]) ->all(), ]; } private function fermentationPracticeLabel(?string $value): ?string { if ($value === null || $value === '') { return null; } return match (true) { in_array($value, [ PraktikFermentasi::RUTIN_SENDIRI->value, PraktikFermentasi::RUTIN_KELOMPOK->value, ], true) || str_starts_with($value, 'Ya, rutin') => 'Ya, rutin', in_array($value, [ PraktikFermentasi::PERNAH_SENDIRI->value, PraktikFermentasi::PERNAH_KELOMPOK->value, ], true) || str_starts_with($value, 'Pernah mencoba') => 'Pernah mencoba', $value === PraktikFermentasi::BELUM_PERNAH->value || str_starts_with($value, 'Belum') => 'Belum pernah', default => null, }; } private function surveyProductivityByUnitChart($rows, string $unit): array { $averages = $rows ->filter(fn ($row) => $row->satuan === $unit && (float) ($row->kuantitas ?? 0) > 0) ->groupBy(fn ($row) => $row->komoditas_nama ?: 'Belum ditentukan') ->map(fn ($items) => round((float) $items->avg(fn ($row) => (float) $row->kuantitas), 2)) ->sortDesc(); return [ 'labels' => $averages->keys()->values()->all(), 'series' => $averages->values()->all(), ]; } private function symptomChart($rows): array { $counts = collect(); $noSymptoms = 0; foreach ($rows as $row) { $symptoms = collect([ $this->enumLabel(SeranganHama::class, $row->serangan_hama ?? null), $this->enumLabel(PenyakitTanaman::class, $row->penyakit_tanaman ?? null), ]) ->merge($this->arrayValues($row->jenis_hama ?? null)) ->merge($this->arrayValues($row->jenis_penyakit ?? null)) ->filter() ->values(); if ($symptoms->isEmpty()) { $noSymptoms++; continue; } foreach ($symptoms as $symptom) { $counts[$symptom] = (int) ($counts[$symptom] ?? 0) + 1; } } if ($noSymptoms > 0) { $counts['Tidak ada gejala'] = $noSymptoms; } $counts = $counts->sortDesc()->take(10); return [ 'labels' => $counts->keys()->values()->all(), 'series' => $counts->values()->map(fn ($value) => (int) $value)->values()->all(), ]; } private function conditionLabel($value): string { return $this->enumLabel(KondisiTanaman::class, $value, 'Belum ditentukan', true); } private function severityLabel($value): string { return $this->enumLabel(KeparahanSerangan::class, $value, 'Belum ditentukan', true); } private function enumLabel(string $enumClass, $value, string $fallback = '', bool $short = false): string { $rawValue = $this->enumValue($value); if ($rawValue === null || $rawValue === '') { return $fallback; } if (method_exists($enumClass, 'tryFrom')) { $cases = $enumClass::cases(); $enumValue = isset($cases[0]) && is_int($cases[0]->value) && is_numeric($rawValue) ? (int) $rawValue : $rawValue; $case = $enumClass::tryFrom($enumValue); if ($case && method_exists($case, 'label')) { $label = $case->label(); return $short ? explode(',', $label)[0] : $label; } } return str($rawValue)->replace('_', ' ')->title()->toString(); } private function enumValue($value): ?string { if ($value instanceof \BackedEnum) { return (string) $value->value; } if ($value === null) { return null; } return (string) $value; } private function arrayValues($value): array { if ($value === null || $value === '') { return []; } if ($value instanceof \Illuminate\Support\Collection) { return $value->values()->all(); } if (is_array($value)) { return array_values(array_filter($value, fn ($item) => $item !== null && $item !== '')); } $decoded = json_decode((string) $value, true); if (is_array($decoded)) { return array_values(array_filter($decoded, fn ($item) => $item !== null && $item !== '')); } return [(string) $value]; } }