From 3a9b3dbd7be46e72054db3d7f6d182b0ba10b11d Mon Sep 17 00:00:00 2001 From: SyaharaniSalsabilla Date: Wed, 14 Oct 2026 19:43:39 +0700 Subject: [PATCH] dashboard produksi --- .../Web/Dashboard/SummaryController.php | 776 ++++++++- resources/views/dashboard/pasar.blade.php | 259 +++ .../views/dashboard/produktivitas.blade.php | 1465 +++++++++++++++-- resources/views/layout/sidebar.blade.php | 7 + routes/web.php | 1 + tests/Feature/ActualProductivityChartTest.php | 108 ++ 6 files changed, 2481 insertions(+), 135 deletions(-) create mode 100644 resources/views/dashboard/pasar.blade.php create mode 100644 tests/Feature/ActualProductivityChartTest.php diff --git a/app/Http/Controllers/Web/Dashboard/SummaryController.php b/app/Http/Controllers/Web/Dashboard/SummaryController.php index d374c51..f9c6e1a 100644 --- a/app/Http/Controllers/Web/Dashboard/SummaryController.php +++ b/app/Http/Controllers/Web/Dashboard/SummaryController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Web\Dashboard; +use App\Enums\AksesPasar\PraktikFermentasi; use App\Enums\KeparahanSerangan; use App\Enums\KondisiTanaman; use App\Enums\PenyakitTanaman; @@ -9,9 +10,14 @@ use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama; use App\Enums\PraktikBudidaya\FrekuensiSanitasiLahan; use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit; use App\Enums\PraktikBudidaya\PemberianPupuk; +use App\Enums\PraktikBudidaya\PraktikSortasi; use App\Enums\SeranganHama; use App\Http\Controllers\Controller; use App\Models\Pertanian\Komoditas; +use App\Models\Wilayah\DesaKelurahan; +use App\Models\Wilayah\KabupatenKota; +use App\Models\Wilayah\Kecamatan; +use App\Models\Wilayah\Provinsi; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; @@ -49,6 +55,89 @@ class SummaryController extends Controller ] + $filterData + $this->productionDashboardData($request)); } + 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); @@ -96,6 +185,8 @@ class SummaryController extends Controller $avgLuasPerPetani = $landMetrics['ratarata_luas_lahan']; $productivityCards = $this->productivityKpiCards($lahanIds); $praktikBudidaya = $this->filteredPraktikBudidaya($request); + $surveyPanen = $this->filteredSurveyPanen($request); + $aksesPasar = $this->filteredAksesPasar($request); $kpiCards = [ [ @@ -119,23 +210,24 @@ class SummaryController extends Controller return [ 'kpiCards' => $kpiCards, + 'mapData' => $this->productionMapData($request), 'chartData' => [ - 'umurTanaman' => $this->ageBucketChart($healthSource, 'umur_tanaman'), + '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->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( + '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->arrayOptionChart( + 'kebiasaanPraktikBaik' => $this->arrayOptionByCommodityChart( $praktikBudidaya, 'kebiasaan_konservasi_tanah', [ @@ -149,10 +241,387 @@ class SummaryController extends Controller ? 'Membuat pematang/tanggul tanah di lahan miring supaya tanah tidak longsor ke bawah' : $value, ), + 'praktikSortasiPanen' => $this->enumOptionChart($surveyPanen, 'praktik_sortasi', PraktikSortasi::class), + 'praktikFermentasiPascapanen' => $this->fermentationPracticeChart($aksesPasar), + 'produktivitasAktualPerKomoditas' => $this->actualProductivityByCommodityChart($request), ], ]; } + 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, + 'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels), + ]; + } + + 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.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'); + + $geoLahan = $mapLahan + ->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path)) + ->map(function ($lahan) use ($tanamanByLahan) { + $tanamanRows = $tanamanByLahan->get($lahan->id, collect()); + $komoditas = $this->productionCommodityDetails($lahan, $tanamanRows); + $opt = $this->productionOptDetails($lahan, $tanamanRows); + + 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, + ]; + }) + ->values() + ->all(); + + return [ + 'provinsi' => array_values($geoProvinsi), + 'kabupaten' => array_values($geoKabupaten), + 'kecamatan' => array_values($geoKecamatan), + 'desa' => array_values($geoDesa), + 'lahan' => $geoLahan, + ]; + } + + private function productionCultivationMapLevelData(Request $request, array $baseLevels): array + { + $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') + ->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.petani_id', + 'profiles.nama as profile_nama', + 'petani.nama as petani_nama', + ]); + $petaniIds = $mapLahan->pluck('petani_id')->filter()->unique()->values(); + $practiceByPetani = DB::table('praktik_budidaya') + ->whereNull('praktik_budidaya.deleted_at') + ->when($petaniIds->isNotEmpty(), fn ($query) => $query->whereIn('praktik_budidaya.petani_id', $petaniIds)) + ->when($petaniIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0')) + ->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 ($practiceByPetani) { + $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, + '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' => $baseLevels['provinsi'] ?? [], + 'kabupaten' => $baseLevels['kabupaten'] ?? [], + 'kecamatan' => $baseLevels['kecamatan'] ?? [], + 'desa' => $baseLevels['desa'] ?? [], + '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); @@ -160,13 +629,53 @@ class SummaryController extends Controller 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([ - 'frekuensi_pemangkasan_utama', - 'pemberian_pupuk', - 'metode_penanganan_opt', - 'frekuensi_sanitasi_lahan', - 'sumber_air', - 'kebiasaan_konservasi_tanah', + '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) + ->get(['praktik_fermentasi']); + } + + 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', ]); } @@ -267,6 +776,54 @@ class SummaryController extends Controller : 0; } + 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 toKilograms(float $value, string $unit): float { return match (strtolower($unit)) { @@ -402,6 +959,64 @@ class SummaryController extends Controller ]; } + 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 @@ -434,6 +1049,40 @@ class SummaryController extends Controller ]; } + 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]); @@ -459,6 +1108,100 @@ class SummaryController extends Controller ]; } + 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 fermentationPracticeChart($rows): array + { + $labels = ['Ya, rutin', 'Pernah mencoba', 'Belum pernah']; + $counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]); + + foreach ($rows as $row) { + $label = $this->fermentationPracticeLabel($row->praktik_fermentasi ?? null); + + if ($label === null) { + continue; + } + + $counts[$label] = (int) $counts[$label] + 1; + } + + return [ + 'labels' => $labels, + 'series' => collect($labels)->map(fn ($label) => (int) ($counts[$label] ?? 0))->values()->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(); @@ -515,7 +1258,12 @@ class SummaryController extends Controller } if (method_exists($enumClass, 'tryFrom')) { - $case = $enumClass::tryFrom($rawValue); + $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(); diff --git a/resources/views/dashboard/pasar.blade.php b/resources/views/dashboard/pasar.blade.php new file mode 100644 index 0000000..6561b3d --- /dev/null +++ b/resources/views/dashboard/pasar.blade.php @@ -0,0 +1,259 @@ +@extends('layout.main') + +@push('style') + +@endpush + +@section('content') +
+
+
+
+
+
+
+ + Komoditas * +
+ +
+ +
+ + + +
+ +
+ +
+
+ + Wilayah +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+ + @if (! $selectedKomoditas) + + +
+
+
+
+ + + +
+
Belum ada data pasar yang ditampilkan
+

Gunakan filter komoditas untuk melihat harga jual rata-rata.

+
+
+
+ @else +
+
+
+
+
+
+ + + +
+ +
+
+

{{ $priceCard['value'] }}

+ {{ $priceCard['unit'] }} +
+ +

{{ $priceCard['label'] }}

+ {{ $priceCard['description'] }} +
+
+ +
+ {{ $priceCard['caption'] }} + {{ number_format($priceCard['records'], 0, ',', '.') }} data harga +
+
+
+
+
+ @endif +
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/dashboard/produktivitas.blade.php b/resources/views/dashboard/produktivitas.blade.php index ec36a41..ae90cb0 100644 --- a/resources/views/dashboard/produktivitas.blade.php +++ b/resources/views/dashboard/produktivitas.blade.php @@ -1,6 +1,7 @@ @extends('layout.main') @push('style') + @endpush @@ -280,13 +600,83 @@ @endforeach + + +
+
+
+
+
+
+
Peta Sebaran
+ Lahan dan tanaman berdasarkan filter wilayah, komoditas, dan segmen +
+
+ + + +
+
+
+
+ +
Belum ada poligon lahan untuk filter ini.
+
+
+
+
+
+ +
+
+
+
+
+
Peta Praktik Budidaya
+ Intensitas pemangkasan, pemupukan, dan kebiasaan praktik baik berdasarkan filter wilayah +
+
+ + + +
+
+
+
+ +
Belum ada poligon lahan untuk filter ini.
+
+
+
+
+
+
+