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', ]), ] + $filterData + $this->marketDashboardData($request)); } private function marketDashboardData(Request $request): array { if (! $request->filled('komoditas_id')) { return [ 'selectedKomoditas' => null, 'priceCard' => null, ]; } $selectedKomoditas = Komoditas::query() ->whereKey($request->komoditas_id) ->first(['id', 'nama']); if (! $selectedKomoditas) { return [ 'selectedKomoditas' => null, 'priceCard' => null, ]; } $productCaption = $this->marketProductCaption($selectedKomoditas->nama); $productLabel = 'Buah segar hasil petik'; $petaniIds = $this->filteredPetaniIdsQuery($request); $priceQuery = DB::table('harga_jual_panen') ->whereIn('harga_jual_panen.petani_id', $petaniIds) ->where('harga_jual_panen.harga_panen_terakhir', '>', 0) ->where('harga_jual_panen.bentuk_produk', $productLabel); $averagePrice = (float) (clone $priceQuery)->avg('harga_jual_panen.harga_panen_terakhir'); $totalRecords = (int) (clone $priceQuery)->count(); return [ 'selectedKomoditas' => $selectedKomoditas, 'priceCard' => [ 'value' => $averagePrice > 0 ? 'Rp '.number_format($averagePrice, 0, ',', '.') : 'Rp 0', 'label' => 'Harga Jual', 'caption' => $productCaption, 'description' => 'Harga rata-rata buah segar hasil petik di tingkat petani', 'records' => $totalRecords, 'unit' => '/kg', 'variant' => 'success', 'icon' => 'tabler-cash', ], ]; } private function marketProductCaption(string $komoditasName): string { $normalized = str($komoditasName)->lower()->trim()->toString(); return match (true) { str_contains($normalized, 'kakao') => 'Harga Buah Kakao', str_contains($normalized, 'vanili') => 'Harga Curing', str_contains($normalized, 'kopi') => 'Harga Ceri Merah', str_contains($normalized, 'kemiri') => 'Harga Gelondong', default => 'Harga Buah Segar', }; } 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]; } }