Files
database-pertani-web/app/Http/Controllers/Web/Dashboard/SummaryController.php
T
2026-10-15 14:12:03 +07:00

1420 lines
57 KiB
PHP

<?php
namespace App\Http\Controllers\Web\Dashboard;
use App\Enums\AksesPasar\PraktikFermentasi;
use App\Enums\KeparahanSerangan;
use App\Enums\KondisiTanaman;
use App\Enums\PenyakitTanaman;
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;
class SummaryController extends Controller
{
public function index()
{
if (!Gate::allows('super_admin') && !Gate::allows('admin')) {
abort(403);
}
$title = 'Summary Dashboard';
return view('dashboard.summary', compact('title'));
}
public function produktivitas(Request $request)
{
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
abort(403);
}
$title = 'Produksi Dashboard';
$filterData = app(IndeksPetaniController::class)->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);
$totalPetani = $landMetrics['total_petani'];
$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($totalPetani, 0, ',', '.'),
'unit' => 'orang',
'label' => 'Jumlah Petani',
'description' => 'Petani sesuai filter yang dipilih',
'icon' => 'tabler-users',
'variant' => 'primary',
],
[
'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->fermentationPracticeChart($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,
'cultivationLevels' => $this->productionCultivationMapLevelData($request, $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.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);
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)
->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',
]);
}
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 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();
$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];
}
}