diff --git a/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php new file mode 100644 index 0000000..2aa64d7 --- /dev/null +++ b/app/Http/Controllers/Web/Dashboard/IndikatorPertanianController.php @@ -0,0 +1,627 @@ +kabupatenOptions(); + $ngadaOption = collect($kabupatenOptions)->first( + fn ($option) => str_contains(strtolower($option['label']), 'ngada') + ); + $initialFilters = $this->selectedFilters($request, $ngadaOption['value'] ?? 'all'); + + $totalPetani = $this->applyPetaniFilters(Petani::query(), $initialFilters)->count(); + $totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $initialFilters)->sum('luas_lahan')) / 10000; + $rataRataLuasLahan = $totalPetani > 0 + ? $totalLuasLahan / $totalPetani + : 0; + $totalProduksiKg = $this->totalProduksiKg($initialFilters); + $rataRataProduksiKg = $totalPetani > 0 + ? $totalProduksiKg / $totalPetani + : 0; + $rataRataProduktivitas = $totalLuasLahan > 0 + ? $totalProduksiKg / $totalLuasLahan + : 0; + + $filterOptions = [ + [ + 'key' => 'kabupaten', + 'label' => 'Kabupaten', + 'options' => $kabupatenOptions, + 'icon' => 'tabler-map-pin', + 'readonly' => true, + ], + [ + 'key' => 'kecamatan', + 'label' => 'Kecamatan', + 'options' => $this->kecamatanOptions(), + ], + [ + 'key' => 'desa', + 'label' => 'Desa', + 'options' => $this->desaOptions(), + ], + [ + 'key' => 'komoditas', + 'label' => 'Komoditas', + 'options' => $this->komoditasOptions(), + ], + [ + 'key' => 'segmentasi', + 'label' => 'Segmentasi', + 'options' => $this->segmentasiOptions(), + ], + ]; + + $summaryData = [ + [ + 'value' => number_format($totalPetani, 0, ',', '.'), + 'label' => 'Petani Terdata', + 'description' => 'Total petani yang masuk sistem', + 'icon' => 'tabler-users', + 'variant' => 'primary', + ], + [ + 'value' => number_format($totalLuasLahan, 2, ',', '.').' ha', + 'label' => 'Total Luas Lahan', + 'description' => 'Rata-rata '.number_format($rataRataLuasLahan, 2, ',', '.').' ha/petani', + 'icon' => 'tabler-mountain', + 'variant' => 'success', + ], + [ + 'value' => number_format($totalProduksiKg, 0, ',', '.').' kg', + 'label' => 'Total Produksi (Semua Komoditas)', + 'description' => 'Rata-rata '.number_format($rataRataProduksiKg, 0, ',', '.').' kg/petani', + 'icon' => 'tabler-wheat', + 'variant' => 'warning', + ], + [ + 'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha', + 'label' => 'Rata-rata Produktivitas', + 'description' => 'Semua komoditas berdasarkan total produksi dan luas lahan', + 'icon' => 'tabler-chart-bar', + 'variant' => 'info', + ], + ]; + + return view('dashboard.indikator-pertanian', [ + 'title' => 'Indikator Pertanian', + 'initialFilters' => $initialFilters, + 'filterOptions' => $filterOptions, + 'summaryData' => $summaryData, + 'chartData' => [ + 'petaniPerKomoditas' => $this->petaniPerKomoditasChart($initialFilters), + 'luasLahanPerKecamatan' => $this->luasLahanPerKecamatanChart($initialFilters), + 'kondisiTanaman' => $this->kondisiTanamanChart($initialFilters), + 'statusKepemilikanLahan' => $this->statusKepemilikanLahanChart($initialFilters), + 'seranganHama' => $this->seranganHamaChart($initialFilters), + 'penyakitTanaman' => $this->penyakitTanamanChart($initialFilters), + 'keparahanSerangan' => $this->keparahanSeranganChart($initialFilters), + 'statusHakKelolaLahan' => $this->statusHakKelolaLahanChart($initialFilters), + 'praktikPemupukan' => $this->praktikPemupukanChart($initialFilters), + ], + ]); + } + + private function selectedFilters(Request $request, string $defaultKabupaten): array + { + $filters = [ + 'kabupaten' => $defaultKabupaten, + 'kecamatan' => $this->validFilterValue($request->query('kecamatan'), Kecamatan::class), + 'desa' => $this->validFilterValue($request->query('desa'), DesaKelurahan::class), + 'komoditas' => $this->validFilterValue($request->query('komoditas'), Komoditas::class), + 'segmentasi' => $this->validSegmentasi((string) $request->query('segmentasi', 'all')), + ]; + + if ($filters['desa'] !== 'all') { + $desa = DesaKelurahan::query()->find($filters['desa']); + + if (! $desa || ($filters['kecamatan'] !== 'all' && (string) $desa->kecamatan_id !== $filters['kecamatan'])) { + $filters['desa'] = 'all'; + } + } + + if ($filters['kecamatan'] === 'all') { + $filters['desa'] = 'all'; + } + + return $filters; + } + + private function validFilterValue(mixed $value, string $modelClass): string + { + if (! $value || $value === 'all') { + return 'all'; + } + + return $modelClass::query()->whereKey($value)->exists() + ? (string) $value + : 'all'; + } + + private function validSegmentasi(string $value): string + { + return in_array($value, [ + 'Penggerak', + 'Perintis', + 'Pemerhati', + 'Pemula', + 'Belum tersegmentasi', + 'all', + ], true) ? $value : 'all'; + } + + private function kabupatenOptions(): array + { + return $this->withAllOption( + 'Semua Kabupaten', + KabupatenKota::query() + ->orderBy('nama') + ->get(['id', 'kode', 'nama']) + ->map(fn (KabupatenKota $kabupaten) => [ + 'value' => (string) $kabupaten->id, + 'label' => $this->kabupatenLabel($kabupaten), + ]) + ->values() + ->all() + ); + } + + private function kecamatanOptions(): array + { + return $this->withAllOption( + 'Semua Kecamatan', + Kecamatan::query() + ->orderBy('nama') + ->get(['id', 'nama']) + ->map(fn (Kecamatan $kecamatan) => [ + 'value' => (string) $kecamatan->id, + 'label' => $kecamatan->nama, + ]) + ->values() + ->all() + ); + } + + private function desaOptions(): array + { + return $this->withAllOption( + 'Semua Desa', + DesaKelurahan::query() + ->orderBy('nama') + ->get(['id', 'nama', 'kecamatan_id']) + ->map(fn (DesaKelurahan $desa) => [ + 'value' => (string) $desa->id, + 'label' => $desa->nama, + 'parent' => (string) $desa->kecamatan_id, + ]) + ->values() + ->all() + ); + } + + private function komoditasOptions(): array + { + return $this->withAllOption( + 'Semua Komoditas', + Komoditas::query() + ->orderBy('nama') + ->get(['id', 'nama']) + ->map(fn (Komoditas $komoditas) => [ + 'value' => (string) $komoditas->id, + 'label' => $komoditas->nama, + ]) + ->values() + ->all() + ); + } + + private function segmentasiOptions(): array + { + $segments = SkorPetani::query() + ->whereNotNull('skor_kesiapan_adopsi') + ->pluck('skor_kesiapan_adopsi') + ->map(fn ($score) => $this->segmentLabel((float) $score)) + ->filter() + ->unique() + ->sortBy(fn ($segment) => array_search($segment, [ + 'Penggerak', + 'Perintis', + 'Pemerhati', + 'Pemula', + 'Belum tersegmentasi', + ], true)) + ->map(fn ($segment) => [ + 'value' => $segment, + 'label' => $segment, + ]) + ->values() + ->all(); + + return $this->withAllOption('Semua Segmentasi', $segments); + } + + private function totalProduksiKg(array $filters): float + { + return $this->applyProduksiFilters(Produksi::query(), $filters) + ->whereNotNull('jumlah_panen') + ->get(['jumlah_panen', 'satuan']) + ->sum(fn (Produksi $produksi) => $this->toKilograms( + (float) $produksi->jumlah_panen, + (string) $produksi->satuan + )); + } + + private function petaniPerKomoditasChart(array $filters): array + { + $rows = $this->applyPetaniFilters(Petani::query(), $filters) + ->leftJoin('master_komoditas as k', 'k.id', '=', 'petani.komoditas_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('COUNT(petani.id) as total') + ->groupBy('k.id', 'k.nama') + ->orderByDesc('total') + ->get(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(), + ]; + } + + private function luasLahanPerKecamatanChart(array $filters): array + { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id') + ->leftJoin('master_kecamatan as k', 'k.id', '=', 'd.kecamatan_id') + ->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label") + ->selectRaw('SUM(map_lahan.luas_lahan) / 10000 as total') + ->groupBy('k.id', 'k.nama') + ->havingRaw('SUM(map_lahan.luas_lahan) > 0') + ->orderByDesc('total') + ->limit(8) + ->get() + ->filter(fn ($row) => round((float) $row->total, 2) > 0) + ->values(); + + return [ + 'labels' => $rows->pluck('label')->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(), + ]; + } + + private function kondisiTanamanChart(array $filters): array + { + $rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters) + ->whereNull('map_tanaman.deleted_at') + ->whereNotNull('map_tanaman.kondisi_tanaman') + ->selectRaw('map_tanaman.kondisi_tanaman as label') + ->selectRaw('COUNT(map_tanaman.id) as total') + ->groupBy('map_tanaman.kondisi_tanaman') + ->orderByDesc('total') + ->get(); + + if ($rows->isEmpty()) { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNull('deleted_at') + ->whereNotNull('kondisi_tanaman') + ->selectRaw('kondisi_tanaman as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy('kondisi_tanaman') + ->orderByDesc('total') + ->get(); + } + + return [ + 'labels' => $rows->pluck('label')->map(fn ($label) => $this->shortEnumLabel((string) $label))->values()->all(), + 'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(), + ]; + } + + private function seranganHamaChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'serangan_hama', + fn ($label) => $this->enumLabel(SeranganHama::class, (string) $label) + ); + } + + private function penyakitTanamanChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'penyakit_tanaman', + fn ($label) => $this->enumLabel(PenyakitTanaman::class, (string) $label) + ); + } + + private function keparahanSeranganChart(array $filters): array + { + return $this->tanamanAttributeChart( + $filters, + 'keparahan_serangan', + fn ($label) => $this->keparahanSeranganLabel((string) $label) + ); + } + + private function statusKepemilikanLahanChart(array $filters): array + { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNotNull('status_kepemilikan') + ->selectRaw('status_kepemilikan as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy('status_kepemilikan') + ->orderByDesc('total') + ->get(); + + return $this->chartFromRows($rows, fn ($label) => $this->shortEnumLabel((string) $label)); + } + + private function statusHakKelolaLahanChart(array $filters): array + { + $rows = $this->applySosialEkonomiFilters(SosialEkonomi::query(), $filters) + ->whereNotNull('status_kepemilikan_lahan') + ->selectRaw('status_kepemilikan_lahan as label') + ->selectRaw('COUNT(sosial_ekonomi.id) as total') + ->groupBy('status_kepemilikan_lahan') + ->orderByDesc('total') + ->get(); + + return $this->chartFromRows( + $rows, + fn ($label) => $this->enumLabel(StatusKepemilikanLahanSosialEkonomi::class, (string) $label) + ); + } + + private function praktikPemupukanChart(array $filters): array + { + $rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters) + ->whereNotNull('pemberian_pupuk') + ->selectRaw('pemberian_pupuk as label') + ->selectRaw('COUNT(praktik_budidaya.id) as total') + ->groupBy('pemberian_pupuk') + ->orderBy('pemberian_pupuk') + ->get(); + + return $this->chartFromRows( + $rows, + fn ($label) => $this->pemberianPupukLabel((int) $label) + ); + } + + private function chartFromRows($rows, callable $labelResolver): array + { + return [ + 'labels' => $rows + ->pluck('label') + ->map(fn ($label) => $labelResolver($label)) + ->values() + ->all(), + 'series' => $rows + ->pluck('total') + ->map(fn ($value) => (int) $value) + ->values() + ->all(), + ]; + } + + private function tanamanAttributeChart(array $filters, string $field, callable $labelResolver): array + { + $qualifiedField = 'map_tanaman.'.$field; + + $rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters) + ->whereNull('map_tanaman.deleted_at') + ->whereNotNull($qualifiedField) + ->selectRaw($qualifiedField.' as label') + ->selectRaw('COUNT(map_tanaman.id) as total') + ->groupBy($qualifiedField) + ->orderByDesc('total') + ->get(); + + if ($rows->isEmpty()) { + $rows = $this->applyLahanFilters(Lahan::query(), $filters) + ->whereNotNull($field) + ->selectRaw($field.' as label') + ->selectRaw('COUNT(map_lahan.id) as total') + ->groupBy($field) + ->orderByDesc('total') + ->get(); + } + + return $this->chartFromRows($rows, $labelResolver); + } + + private function applyPetaniFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan'])); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereHas('skor', function ($skor) use ($filters) { + $this->applySegmentasiRange($skor, $filters['segmentasi']); + }); + }); + } + + private function applyLahanFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan'])); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereIn('petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])); + }); + } + + private function applyProduksiFilters($query, array $filters) + { + return $query + ->when($filters['kecamatan'] !== 'all' || $filters['desa'] !== 'all' || $filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->whereHas('lahan', function ($lahan) use ($filters) { + $this->applyLahanFilters($lahan, $filters); + }); + }) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas'])); + } + + private function applySosialEkonomiFilters($query, array $filters) + { + return $query->whereHas('petani', function ($petani) use ($filters) { + $this->applyPetaniFilters($petani, $filters); + }); + } + + private function applyPraktikBudidayaFilters($query, array $filters) + { + return $query->whereHas('petani', function ($petani) use ($filters) { + $this->applyPetaniFilters($petani, $filters); + }); + } + + private function applyTanamanTableFilters($query, array $filters) + { + return $query + ->leftJoin('map_lahan as l', 'l.id', '=', 'map_tanaman.lahan_id') + ->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) { + $query + ->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'l.desa_kelurahan_id') + ->where('d.kecamatan_id', $filters['kecamatan']); + }) + ->when($filters['desa'] !== 'all', fn ($query) => $query->where('l.desa_kelurahan_id', $filters['desa'])) + ->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('map_tanaman.komoditas_id', $filters['komoditas'])) + ->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) { + $query->where(function ($query) use ($filters) { + $query + ->whereIn('map_tanaman.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])) + ->orWhereIn('l.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi'])); + }); + }); + } + + private function segmentasiPetaniIdsQuery(string $segmentasi) + { + return Petani::query() + ->select('id') + ->whereHas('skor', function ($skor) use ($segmentasi) { + $this->applySegmentasiRange($skor, $segmentasi); + }); + } + + private function applySegmentasiRange($query, string $segmentasi) + { + return match ($segmentasi) { + 'Penggerak' => $query->where('skor_kesiapan_adopsi', '>=', 4), + 'Perintis' => $query->where('skor_kesiapan_adopsi', '>=', 3)->where('skor_kesiapan_adopsi', '<', 4), + 'Pemerhati' => $query->where('skor_kesiapan_adopsi', '>=', 2)->where('skor_kesiapan_adopsi', '<', 3), + 'Pemula' => $query->where('skor_kesiapan_adopsi', '>=', 1)->where('skor_kesiapan_adopsi', '<', 2), + default => $query->where(function ($query) { + $query->whereNull('skor_kesiapan_adopsi')->orWhere('skor_kesiapan_adopsi', '<', 1); + }), + }; + } + + private function shortEnumLabel(string $value): string + { + return str($value) + ->replace('_', ' ') + ->headline() + ->toString(); + } + + private function enumLabel(string $enumClass, int|string $value): string + { + $case = $enumClass::tryFrom($value); + + if (! $case) { + return $this->shortEnumLabel((string) $value); + } + + return method_exists($case, 'label') + ? $case->label() + : $this->shortEnumLabel((string) $case->value); + } + + private function keparahanSeranganLabel(string $value): string + { + return match (KeparahanSerangan::tryFrom($value)) { + KeparahanSerangan::RINGAN => 'Ringan', + KeparahanSerangan::SEDANG => 'Sedang', + KeparahanSerangan::BERAT => 'Parah', + default => $this->shortEnumLabel($value), + }; + } + + private function pemberianPupukLabel(int $value): string + { + return match (PemberianPupuk::tryFrom($value)) { + PemberianPupuk::TIDAK_PERNAH => 'Tidak pernah', + PemberianPupuk::JENIS_DOSIS_WAKTU_TIDAK_MENENTU => 'Tidak menentu', + PemberianPupuk::JENIS_SESUAI_DOSIS_TIDAK_MENENTU => 'Jenis sesuai', + PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT => 'Cukup tepat', + PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_TEPAT => 'Tepat & rutin', + default => $this->shortEnumLabel((string) $value), + }; + } + + private function toKilograms(float $value, string $unit): float + { + return match (strtolower($unit)) { + 'ton' => $value * 1000, + 'kuintal', 'kwintal' => $value * 100, + default => $value, + }; + } + + private function withAllOption(string $label, array $options): array + { + return array_merge([ + [ + 'value' => 'all', + 'label' => $label, + ], + ], $options); + } + + private function kabupatenLabel(KabupatenKota $kabupaten): string + { + return preg_replace('/^(kabupaten|kota)\s+/i', '', $kabupaten->nama); + } + + private function segmentLabel(float $score): string + { + return match (true) { + $score >= 4.0 => 'Penggerak', + $score >= 3.0 => 'Perintis', + $score >= 2.0 => 'Pemerhati', + $score >= 1.0 => 'Pemula', + default => 'Belum tersegmentasi', + }; + } +} diff --git a/resources/views/dashboard/indikator-pertanian.blade.php b/resources/views/dashboard/indikator-pertanian.blade.php new file mode 100644 index 0000000..9d0af4b --- /dev/null +++ b/resources/views/dashboard/indikator-pertanian.blade.php @@ -0,0 +1,1159 @@ +@extends('layout.main') + +@push('style') + +@endpush + +@section('content') +
+ Kondisi dan performa usaha tani petani berdasarkan hasil survei +
++ {{ $summary['label'] }} +
+ ++ + {{ $summary['description'] }} + +
+