dashboard produksi

This commit is contained in:
2026-10-09 13:22:51 +07:00
parent 8a2c0292ac
commit f49bb0c2db
4 changed files with 814 additions and 721 deletions
@@ -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];
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.produktivitas') ? 'active' : '' }}">
<a href="{{ route('dashboard.produktivitas') }}" class="menu-link">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-chart-bar"></i></span><div data-i18n="Produktivitas">Produktivitas</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-chart-bar"></i></span><div data-i18n="Produksi">Produksi</div>
</a>
</li>
</ul>
@@ -83,7 +83,7 @@
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
<div class="container-fluid flex-grow-1 container-p-y">
<div class="d-flex flex-row gap-3 mb-4">
<div class="w-25">
<x-summary-card