diff --git a/app/Http/Controllers/Web/Dashboard/SummaryController.php b/app/Http/Controllers/Web/Dashboard/SummaryController.php index 4875199..c545f94 100644 --- a/app/Http/Controllers/Web/Dashboard/SummaryController.php +++ b/app/Http/Controllers/Web/Dashboard/SummaryController.php @@ -2,14 +2,13 @@ namespace App\Http\Controllers\Web\Dashboard; +use App\Enums\KeparahanSerangan; +use App\Enums\KondisiTanaman; +use App\Enums\PenyakitTanaman; +use App\Enums\SeranganHama; use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use App\Models\Profile; -use App\Models\Wilayah\Provinsi; -use App\Models\wilayah\KabupatenKota; -use App\Models\Wilayah\Kecamatan; -use App\Models\Wilayah\DesaKelurahan; -use App\Models\Pertanian\Lahan; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; class SummaryController extends Controller @@ -26,121 +25,352 @@ class SummaryController extends Controller public function produktivitas(Request $request) { if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) { - abort(403); - } - - $title = 'Produktivitas Dashboard'; - $luasTotal = Lahan::whereNotNull('path')->sum('luas_lahan'); - $totalpetani = Profile::where('role', 'petani')->count(); - $totallahan = Lahan::whereNotNull('path')->count(); - - $petaniList = Profile::select('id', 'nama', 'role', 'desa_kelurahan_id') - ->where('role', 'petani') - ->with([ - 'lahan:id,profile_id,luas_lahan', - 'desaKelurahan:id,nama,kecamatan_id', - ]) - ->paginate(20); - - $lahanSummaryByDesa = Lahan::whereNotNull('path') - ->whereNotNull('desa_kelurahan_id') - ->select('desa_kelurahan_id') - ->selectRaw('COUNT(*) as total_lahan') - ->selectRaw('COALESCE(SUM(luas_lahan), 0) as total_luas') - ->groupBy('desa_kelurahan_id') - ->get(); - - $desaList = DesaKelurahan::select('id', 'nama', 'path', 'kecamatan_id') - ->whereIn('id', $lahanSummaryByDesa->pluck('desa_kelurahan_id')->filter()->unique()->values()) - ->with([ - 'kecamatan:id,nama,path,kabupaten_kota_id', - 'kecamatan.kabupatenKota:id,nama,path,provinsi_id', - 'kecamatan.kabupatenKota.provinsi:id,nama,path', - ]) - ->get() - ->keyBy('id'); - - $mapLahanLimit = min(max((int) $request->integer('map_lahan_limit', 500), 0), 2000); - $mapLahan = Lahan::whereNotNull('path') - ->select('id', 'profile_id', 'desa_kelurahan_id', 'path', 'luas_lahan') - ->with([ - 'profile:id,nama', - 'tanaman:id,lahan_id,komoditas_id', - 'tanaman.komoditas:id,nama', - ]) - ->orderByDesc('updated_at') - ->limit($mapLahanLimit) - ->get(); - - $geoProvinsi = []; - $geoKabupaten = []; - $geoKecamatan = []; - $geoDesa = []; - $geoLahan = []; - - foreach ($lahanSummaryByDesa as $summary) { - $desa = $desaList->get($summary->desa_kelurahan_id); - $kec = $desa ? $desa->kecamatan : null; - $kab = $kec ? $kec->kabupatenKota : null; - $prov = $kab ? $kab->provinsi : null; - $totalLahan = (int) $summary->total_lahan; - $totalLuas = (float) $summary->total_luas; - - if ($prov && $prov->path) { - if (!isset($geoProvinsi[$prov->id])) { - $geoProvinsi[$prov->id] = ['id' => $prov->id, 'nama' => $prov->nama, 'polygon' => $prov->path, 'total_lahan' => 0, 'total_luas' => 0]; - } - $geoProvinsi[$prov->id]['total_lahan'] += $totalLahan; - $geoProvinsi[$prov->id]['total_luas'] += $totalLuas; - } - - if ($kab && $kab->path) { - if (!isset($geoKabupaten[$kab->id])) { - $geoKabupaten[$kab->id] = ['id' => $kab->id, 'parent_id' => $prov->id ?? null, 'nama' => $kab->nama, 'polygon' => $kab->path, 'total_lahan' => 0, 'total_luas' => 0]; - } - $geoKabupaten[$kab->id]['total_lahan'] += $totalLahan; - $geoKabupaten[$kab->id]['total_luas'] += $totalLuas; - } - - if ($kec && $kec->path) { - if (!isset($geoKecamatan[$kec->id])) { - $geoKecamatan[$kec->id] = ['id' => $kec->id, 'parent_id' => $kab->id ?? null, 'nama' => $kec->nama, 'polygon' => $kec->path, 'total_lahan' => 0, 'total_luas' => 0]; - } - $geoKecamatan[$kec->id]['total_lahan'] += $totalLahan; - $geoKecamatan[$kec->id]['total_luas'] += $totalLuas; - } - - if ($desa && $desa->path) { - if (!isset($geoDesa[$desa->id])) { - $geoDesa[$desa->id] = ['id' => $desa->id, 'parent_id' => $kec->id ?? null, 'nama' => $desa->nama, 'polygon' => $desa->path, 'total_lahan' => 0, 'total_luas' => 0]; - } - $geoDesa[$desa->id]['total_lahan'] += $totalLahan; - $geoDesa[$desa->id]['total_luas'] += $totalLuas; - } + abort(403); } - foreach ($mapLahan as $lahan) { - $geoLahan[] = [ - 'id' => $lahan->id, - 'parent_id' => $lahan->desa_kelurahan_id, - 'polygon' => $lahan->path, - 'nama_petani' => $lahan->profile->nama ?? 'N/A', - 'luas' => $lahan->luas_lahan, - 'tanaman' => $lahan->tanaman->map(fn($t) => $t->komoditas->nama ?? null)->filter()->unique()->values() - ]; - } + $title = 'Produksi Dashboard'; + $filterData = app(IndeksPetaniController::class)->contentData($request); return view('dashboard.produktivitas', [ 'title' => $title, - 'petaniList' => $petaniList, - 'totalpetani' => $totalpetani, - 'totallahan' => $totallahan, - 'luasTotal' => $luasTotal, - 'geoProvinsi' => array_values($geoProvinsi), - 'geoKabupaten' => array_values($geoKabupaten), - 'geoKecamatan' => array_values($geoKecamatan), - 'geoDesa' => array_values($geoDesa), - 'geoLahan' => $geoLahan - ]); + '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', + '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']; + $petaniCount = $landMetrics['total_petani']; + $avgLuasPerPetani = $landMetrics['ratarata_luas_lahan']; + $jumlahTanaman = $conditionSource->whereNotNull('kondisi_tanaman')->count(); + + $kpiCards = [ + [ + 'value' => number_format($totalLuasLahan, 2, ',', '.'), + 'unit' => 'Ha', + 'label' => 'Total Luas Lahan', + 'description' => 'Luas keseluruhan lahan terfilter', + 'icon' => 'tabler-chart-area-line', + 'variant' => 'success', + ], + [ + 'value' => number_format($avgLuasPerPetani, 2, ',', '.'), + 'unit' => 'Ha/petani', + 'label' => 'Rata-Rata Luas Lahan per Petani', + 'description' => number_format($petaniCount, 0, ',', '.').' petani terfilter', + 'icon' => 'tabler-ruler-measure', + 'variant' => 'danger', + ], + [ + 'value' => number_format($jumlahTanaman, 0, ',', '.'), + 'unit' => 'tanaman', + 'label' => 'Jumlah Tanaman', + 'description' => 'Total tanaman pada chart kondisi tanaman', + 'icon' => 'tabler-plant-2', + 'variant' => 'secondary', + ], + ]; + + return [ + 'kpiCards' => $kpiCards, + 'chartData' => [ + 'umurLahan' => $this->ageBucketChart($lahan, 'umur_lahan'), + 'umurTanaman' => $this->ageBucketChart($healthSource, 'umur_tanaman'), + '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), + ], + ]; + } + + 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 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]; } } diff --git a/resources/views/dashboard/produktivitas.blade.php b/resources/views/dashboard/produktivitas.blade.php index 91b2c36..591d256 100644 --- a/resources/views/dashboard/produktivitas.blade.php +++ b/resources/views/dashboard/produktivitas.blade.php @@ -1,333 +1,244 @@ @extends('layout.main') @push('style') - @endpush @section('content')
-
-
-
-
-
-
- -
-
- Total Land Area -

{{ number_format($luasTotal, 1) }} ha

-
-
-
- -
-
-
-
- -
-
- Total Lahan -

{{ $totallahan }}

-
-
-
- -
-
-
-
- Active -
-
- Registered Farmers -

{{ $totalpetani }}

-
-
-
- -
-
Distribusi Komoditas
-
- @foreach(['Vanilla' => 45, 'Candlenut' => 30, 'Cocoa' => 15, 'Coffee' => 10] as $name => $val) -
-
- {{ $name }} - {{ $val }}% +
+
+
+
+
+
+ + Wilayah
-
-
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
- @endforeach + +
+ +
+
+ + Data +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ +
+ +
+ @foreach ($kpiCards as $card) +
+
+
+
+
+ + + +
+ +
+
+

{{ $card['value'] }}

+ @if (!empty($card['unit'])) + {{ $card['unit'] }} + @endif +
+ +

{{ $card['label'] }}

+ + {{ $card['description'] }} +
+
+
+
+
+ @endforeach +
+ +
+
+
Kondisi Kebun
+

Ringkasan umur lahan, umur tanaman, kondisi tanaman, gejala terlihat, dan tingkat keparahan serangan.

+
+ +
+ +
+
+
+
+
+
Umur Lahan
+ Kelompok umur lahan terdata +
+
+
+
+
+
+ +
+
+
+
Umur Tanaman
+ Kelompok umur tanaman saat ini +
+
+
+
+
+
+ +
+
+
+
Kondisi Tanaman
+ Distribusi kondisi baik, sedang, buruk, atau mati +
+
+
+
-
-
-
-
Peta Sebaran Lahan
-
- - - +
+
+
+
+
Gejala Serangan yang Terlihat
+ Gabungan data hama, penyakit, dan gejala otomatis +
+
+
-
-
-
-
-
-
Produktivitas Petani Regional
- -
-
- - - - - - - - - - - - @foreach($petaniList as $p) - - - - - - - - @endforeach - -
Nama PetaniID LahanKomoditasLuas (HA)Status
-
-
{{ substr($p->nama, 0, 1) }}
- {{ $p->nama }} -
-
TR-{{ $p->id }}Vanilla{{ number_format($p->lahan->sum('luas_lahan'), 1) }}Verified
+
+
+
+
Keparahan Serangan
+ Ringan, sedang, dan parah +
+
+
+
@@ -336,312 +247,264 @@ @endsection @push('scripts') - -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/layout/sidebar.blade.php b/resources/views/layout/sidebar.blade.php index 0bef129..dd14d90 100644 --- a/resources/views/layout/sidebar.blade.php +++ b/resources/views/layout/sidebar.blade.php @@ -47,7 +47,7 @@ diff --git a/resources/views/produktivitas/petani/index.blade.php b/resources/views/produktivitas/petani/index.blade.php index eced01a..5205818 100644 --- a/resources/views/produktivitas/petani/index.blade.php +++ b/resources/views/produktivitas/petani/index.blade.php @@ -83,7 +83,7 @@ @endpush @section('content') -
+