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)); } 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']; $productivityCards = $this->productivityKpiCards($lahanIds); $praktikBudidaya = $this->filteredPraktikBudidaya($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, 'chartData' => [ 'umurTanaman' => $this->ageBucketChart($healthSource, '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->enumOptionChart($praktikBudidaya, 'frekuensi_pemangkasan_utama', FrekuensiPemangkasanUtama::class), 'intensitasPemupukan' => $this->enumOptionChart($praktikBudidaya, 'pemberian_pupuk', PemberianPupuk::class), 'metodePenangananHamaPenyakit' => $this->enumOptionChart($praktikBudidaya, 'metode_penanganan_opt', MetodePenangananHamaPenyakit::class), 'intensitasPembersihanLahan' => $this->enumOptionChart($praktikBudidaya, 'frekuensi_sanitasi_lahan', FrekuensiSanitasiLahan::class), 'sumberAirKebun' => $this->arrayOptionChart( $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->arrayOptionChart( $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, ), ], ]; } 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) ->get([ 'frekuensi_pemangkasan_utama', 'pemberian_pupuk', 'metode_penanganan_opt', 'frekuensi_sanitasi_lahan', 'sumber_air', 'kebiasaan_konservasi_tanah', ]); } 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($lahanIds): array { $komoditas = Komoditas::query() ->where('is_utama', true) ->orderBy('nama') ->limit(4) ->get(['id', 'nama']); $produksiRows = DB::table('produksi') ->whereNull('produksi.deleted_at') ->leftJoin('map_lahan as lahan_produksi', 'lahan_produksi.id', '=', 'produksi.lahan_id') ->when($lahanIds->isNotEmpty(), fn ($query) => $query->whereIn('produksi.lahan_id', $lahanIds)) ->when($lahanIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0')) ->selectRaw('COALESCE(produksi.komoditas_id, lahan_produksi.komoditas_id) as komoditas_id') ->addSelect([ 'produksi.jumlah_panen', 'produksi.satuan', 'produksi.luas_panen', 'produksi.produktivitas', ]) ->get() ->groupBy('komoditas_id'); $variants = ['primary', 'warning', 'info', 'secondary']; $icon = 'tabler-chart-bar'; return $komoditas ->values() ->map(function ($item, int $index) use ($produksiRows, $variants, $icon) { $rows = $produksiRows->get($item->id, collect()); $average = $this->averageProductivity($rows); return [ 'value' => number_format($average, 0, ',', '.'), 'unit' => 'kg/ha', 'label' => 'Produktivitas Rata-rata '.$item->nama, 'description' => 'Rata-rata produktivitas per komoditas', 'icon' => $icon, 'variant' => $variants[$index % count($variants)], ]; }) ->all(); } private function averageProductivity($rows): float { if ($rows->isEmpty()) { return 0; } $storedAverage = $rows ->pluck('produktivitas') ->filter(fn ($value) => (float) $value > 0) ->avg(); if ($storedAverage > 0) { return round((float) $storedAverage, 2); } $totalProduksiKg = $rows->sum(fn ($row) => $this->toKilograms( (float) ($row->jumlah_panen ?? 0), (string) ($row->satuan ?? 'kg') )); $totalLuasPanen = $rows->sum(fn ($row) => (float) ($row->luas_panen ?? 0)); return $totalLuasPanen > 0 ? round($totalProduksiKg / $totalLuasPanen, 2) : 0; } private function toKilograms(float $value, string $unit): float { return match (strtolower($unit)) { 'ton' => $value * 1000, 'kuintal', 'kwintal' => $value * 100, default => $value, }; } 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 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 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 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')) { $case = $enumClass::tryFrom($rawValue); 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]; } }