33 Commits
Author SHA1 Message Date
Syaharani 013cb7b728 dashboard pasar 2026-10-21 10:14:49 +07:00
Althaf 4ab745cfe5 sumber informasi harga 2026-10-19 10:03:05 +07:00
Althaf c66eed56e2 Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web 2026-10-16 15:11:49 +07:00
Syaharani 8be0906a2e Update profil petani 2026-10-16 15:48:42 +07:00
afriadirahman fcd79b7cfe Update dashboard Profil Detail Petani 2026-10-16 15:25:21 +07:00
Althaf bea9be8a39 resolve aksespasar 2026-10-16 15:11:39 +07:00
afriadirahman 5220b10de7 Update Config js 2026-10-16 10:38:30 +07:00
Syaharani 24725e3df1 update chart dashboard 2026-10-16 10:29:42 +07:00
Syaharani 60d32a88e9 update dashboard produksi 2026-10-16 10:04:56 +07:00
Althaf 4370d3c3d0 Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web 2026-10-15 15:14:57 +07:00
Althaf 7699430301 new field akses pasar 2026-10-15 15:10:18 +07:00
fir2be df2b5a1312 hover and scroll on map 2026-10-15 14:12:03 +07:00
fir2be 2e8271d27a cars 2026-10-15 14:05:01 +07:00
fir2be 9b5c0a7309 perubahan query 2026-10-15 13:02:42 +07:00
fir2be 9d56b67998 sroll untuk 2026-10-15 11:31:57 +07:00
fir2be f95f43999a peta sebaran dan praktek budaya 2026-10-15 11:25:38 +07:00
Syaharani 3e172cf915 card dashboard produksi 2026-10-14 20:18:15 +07:00
Syaharani 3a9b3dbd7b dashboard produksi 2026-10-14 19:43:39 +07:00
Syaharani 80f0056efe dashboard produksi & ganti warna chart 2026-10-13 15:55:18 +07:00
afriadirahman aef27704f2 Update Karakteristik Petani 2026-10-12 10:22:01 +07:00
Syaharani f49bb0c2db dashboard produksi 2026-10-09 13:22:51 +07:00
afriadirahman 8a2c0292ac Update Header 2026-10-08 09:58:56 +07:00
afriadirahman 9ea324e553 Update Dashboard & Sidebar 2026-10-08 09:48:00 +07:00
afriadirahman 94c3288ea7 Update Dashboard Karakteristik Petani 2026-10-07 13:53:55 +07:00
Syaharani 8539bdcec4 update profil petani 2026-10-07 12:03:02 +07:00
Syaharani 77de648ded dashboard karakteristik petani 2026-10-07 10:20:20 +07:00
Syaharani 393cf0d0d1 karakteristik petani 2026-10-06 17:52:34 +07:00
Syaharani 7fdf9b400d solve jumlah lahan dan tanaman 2026-09-23 17:01:19 +07:00
Syaharani be4d46004e solve jumlah lahan dan tanaman 2026-09-23 16:36:14 +07:00
Syaharani fef5358402 solve filter jumlah lahan dan tanaman 2026-09-23 14:52:30 +07:00
Syaharani 02eed6da34 solve jumlah lahan dan tanaman 2026-09-23 14:37:08 +07:00
Syaharani 94d3f2a582 lower filter profil petani 2026-09-23 10:19:30 +07:00
Syaharani 471ca5ac0e filter profil petani 2026-09-23 10:13:13 +07:00
55 changed files with 12877 additions and 4556 deletions
@@ -8,7 +8,7 @@ enum FrekuensiPemangkasanPendamping: string
case TIDAK_RUTIN = 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya';
case SATU_KALI_TAHUN = 'Memangkas 1 kali/tahun, tahu jenis tekniknya';
case DUA_KALI_TAHUN = 'Memangkas 2 kali/tahun, tahu jenis tekniknya';
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin';
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin';
public function label(): string
{
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanPendamping: string
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
};
}
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanUtama: int
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
};
}
@@ -16,8 +16,8 @@ enum MetodePenangananHamaPenyakit: int
self::TIDAK_ADA => 'Tidak ada penanganan',
self::CARA_TRADISIONAL => 'Cara tradisional (doa/ritual)',
self::PESTISIDA_SANITASI => 'Pestisida atau sanitasi',
self::KOMBINASI => 'Kombinasi pestisada dan sanitasi',
self::PHT => 'Pengendalian hama terpadu(PHT)',
self::KOMBINASI => 'Kombinasi pestisida dan sanitasi',
self::PHT => 'Pengendalian hama terpadu (PHT)',
};
}
@@ -11,6 +11,7 @@ use App\Models\Pertanian\Petani;
use App\Models\Pertanian\Komoditas;
use App\Models\Pertanian\Lahan;
use App\Models\Pertanian\PendapatanKomoditas;
use App\Models\Pertanian\SosialEkonomi;
class DemografiPetaniController extends Controller
{
@@ -20,7 +21,56 @@ class DemografiPetaniController extends Controller
abort(403);
}
$petaniCounts = Petani::selectRaw("
return redirect()->route('dashboard.karakteristik-petani', array_merge($request->query(), [
'tab' => 'demografi-petani',
]));
}
public function karakteristik(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$activeTab = $request->query('tab') === 'indeks-petani'
? 'indeks-petani'
: 'demografi-petani';
$demografiData = $this->contentData($request);
$indeksData = app(IndeksPetaniController::class)->contentData($request);
$tableFilters = [
'gender' => ['Gender', ['1' => 'Laki-laki', '0' => 'Perempuan']],
'umur' => ['Kelompok Umur', ['kurang_30' => '< 30 tahun', '30_39' => '30–39 tahun', '40_49' => '40–49 tahun', '50_59' => '50–59 tahun', '60_keatas' => '≥ 60 tahun']],
'pendidikan' => ['Pendidikan', ['tidak_sekolah' => 'Tidak Sekolah', 'sd' => 'SD', 'smp' => 'SMP', 'sma' => 'SMA', 'diploma' => 'Diploma', 's1_keatas' => 'Sarjana ke atas']],
'jumlah_tanggungan' => ['Jumlah Tanggungan', ['0' => '0 orang', '1_2' => '1–2 orang', '3_4' => '3–4 orang', '5_keatas' => '≥ 5 orang']],
'kelompok_tani' => ['Keanggotaan Kelompok Tani', ['terdaftar' => 'Terdaftar', 'tidak_terdaftar' => 'Tidak Terdaftar', 'belum_terisi' => 'Belum Terisi']],
'komoditas' => ['Komoditas', $indeksData['komoditas']->pluck('nama', 'id')->all()],
'luas_lahan' => ['Luas Lahan', ['kurang_05' => '< 0,5 ha', '05_1' => '0,5–1 ha', '1_2' => '> 1–2 ha', 'lebih_2' => '> 2 ha']],
'status_kepemilikan_lahan' => ['Status Kepemilikan Lahan', ['sendiri' => 'Milik Pribadi', 'sewa' => 'Sewa', 'adat' => 'Tanah Adat']],
'penghasilan' => ['Rentang Pendapatan', ['kurang_1' => '< Rp1 juta', '1_3' => 'Rp1–3 juta', '3_5' => '> Rp3–5 juta', '5_10' => '> Rp5–10 juta', 'lebih_10' => '> Rp10 juta']],
];
return view('dashboard.karakteristik-petani', [
'activeTab' => $activeTab,
'tableFilters' => $tableFilters,
'dashboardFilters' => $request->only([
'provinsi_id',
'kabupatenKota_id',
'kecamatan_id',
'desaKelurahan_id',
'komoditas_id',
'segmen',
]),
'indeksFilterAction' => route('dashboard.karakteristik-petani'),
] + $demografiData + $indeksData);
}
public function contentData(Request $request): array
{
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
$petaniCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)->selectRaw("
COUNT(*) as total,
COUNT(*) FILTER (WHERE jenis_kelamin = true) as laki,
COUNT(*) FILTER (WHERE jenis_kelamin = false) as perempuan
@@ -30,14 +80,18 @@ class DemografiPetaniController extends Controller
$petaniLakilaki = $petaniCounts->laki;
$petaniPerempuan = $petaniCounts->perempuan;
$totalKomoditas = Petani::whereNotNull('komoditas_id')
->distinct()
->count('komoditas_id');
$totalKomoditas = Komoditas::query()
->where('is_utama', true)
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
->count();
$totalLuasLahan = Lahan::has('petani')->sum('luas_lahan');
$totalLuasLahan = Lahan::has('petani')
->whereIn('petani_id', clone $filteredPetaniIds)
->sum('luas_lahan');
$ratarataLuasLahan = $totalLuasLahan && $totalPetani ? $totalLuasLahan / $totalPetani : 0;
$umurCounts = Petani::whereNotNull('tanggal_lahir')->selectRaw("
$umurCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)
->whereNotNull('tanggal_lahir')->selectRaw("
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) < 30) as kurang_30,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 30 AND 39) as umur_30_39,
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 40 AND 49) as umur_40_49,
@@ -45,7 +99,8 @@ class DemografiPetaniController extends Controller
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) >= 60) as umur_60_keatas
")->first();
$pendidikanCounts = Petani::whereNotNull('pendidikan_terakhir')
$pendidikanCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)
->whereNotNull('pendidikan_terakhir')
->select('pendidikan_terakhir', DB::raw('COUNT(*) as total'))
->groupBy('pendidikan_terakhir')
->pluck('total', 'pendidikan_terakhir');
@@ -59,20 +114,71 @@ class DemografiPetaniController extends Controller
's1_keatas' => (int) ($pendidikanCounts['06_sarjana'] ?? 0) + (int) ($pendidikanCounts['07_magister'] ?? 0),
];
$jumlahTanggunganBuckets = SosialEkonomi::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->whereNotNull('jumlah_tanggungan')
->selectRaw("
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan = 0) as tanggungan_0,
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan BETWEEN 1 AND 2) as tanggungan_1_2,
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan BETWEEN 3 AND 4) as tanggungan_3_4,
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan >= 5) as tanggungan_5_keatas
")
->first();
$jumlahTanggungan = [
'0' => (int) ($jumlahTanggunganBuckets->tanggungan_0 ?? 0),
'1_2' => (int) ($jumlahTanggunganBuckets->tanggungan_1_2 ?? 0),
'3_4' => (int) ($jumlahTanggunganBuckets->tanggungan_3_4 ?? 0),
'5_keatas' => (int) ($jumlahTanggunganBuckets->tanggungan_5_keatas ?? 0),
];
$kelompokTaniBuckets = Petani::query()
->whereIn('petani.id', clone $filteredPetaniIds)
->leftJoin('sosial_ekonomi', function ($join) {
$join->on('sosial_ekonomi.petani_id', '=', 'petani.id')
->whereNull('sosial_ekonomi.deleted_at');
})
->selectRaw("
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani = 'ya') as terdaftar,
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani = 'tidak') as tidak_terdaftar,
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani IS NULL OR sosial_ekonomi.terdaftar_kelompok_tani NOT IN ('ya', 'tidak')) as belum_terisi
")
->first();
$kelompokTani = [
'terdaftar' => (int) ($kelompokTaniBuckets->terdaftar ?? 0),
'tidak_terdaftar' => (int) ($kelompokTaniBuckets->tidak_terdaftar ?? 0),
'belum_terisi' => (int) ($kelompokTaniBuckets->belum_terisi ?? 0),
];
$desaSasaranRows = Petani::query()
->leftJoin('master_desa_kelurahan as desa', 'petani.desa_kelurahan_id', '=', 'desa.id')
->whereIn('petani.id', clone $filteredPetaniIds)
->selectRaw('petani.desa_kelurahan_id as desa_id')
->selectRaw("COALESCE(desa.nama, 'Tidak diketahui') as desa")
->selectRaw('COUNT(*) as total')
->groupBy('petani.desa_kelurahan_id', 'desa.nama')
->orderByDesc('total')
->orderBy('desa')
->get();
$komoditasUtama = Komoditas::query()
->whereHas('petani')
->withCount('petani')
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
->withCount(['petani' => fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds)])
->orderByDesc('petani_count')
->get(['id', 'nama']);
$luasLahanBuckets = DB::query()->fromSub(
Lahan::query()->select('petani_id')->selectRaw('SUM(luas_lahan) as total_luas')->groupBy('petani_id'),
Lahan::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->select('petani_id')
->selectRaw('luas_lahan'),
'agg'
)->selectRaw("
COUNT(*) FILTER (WHERE total_luas < 5000) as kurang_05,
COUNT(*) FILTER (WHERE total_luas >= 5000 AND total_luas <= 10000) as luas_05_1,
COUNT(*) FILTER (WHERE total_luas > 10000 AND total_luas <= 20000) as luas_1_2,
COUNT(*) FILTER (WHERE total_luas > 20000) as lebih_2
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan < 0.5) as kurang_05,
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan >= 0.5 AND luas_lahan <= 1) as luas_05_1,
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 1 AND luas_lahan <= 2) as luas_1_2,
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 2) as lebih_2
")->first();
$luasLahan = [
@@ -82,14 +188,27 @@ class DemografiPetaniController extends Controller
'lebih_2' => $luasLahanBuckets->lebih_2,
];
$statusKepemilikanLahanRows = Lahan::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->whereNotNull('status_kepemilikan')
->selectRaw('status_kepemilikan as status')
->selectRaw('COUNT(DISTINCT petani_id) as total')
->groupBy('status_kepemilikan')
->orderByDesc('total')
->get();
$pendapatanPerPetaniQuery = PendapatanKomoditas::query()
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
->whereNotNull('sosial_ekonomi.petani_id')
->whereIn('sosial_ekonomi.petani_id', clone $filteredPetaniIds)
->groupBy('sosial_ekonomi.petani_id')
->selectRaw('SUM(pendapatan_komoditas.pendapatan) as total_pendapatan');
$penghasilanBuckets = DB::query()->fromSub(
PendapatanKomoditas::query()
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
->whereNotNull('sosial_ekonomi.petani_id')
->groupBy('sosial_ekonomi.petani_id')
->selectRaw('SUM(pendapatan_komoditas.pendapatan) as total_pendapatan'),
clone $pendapatanPerPetaniQuery,
'agg'
)->selectRaw("
AVG(total_pendapatan) as rata_rata,
COUNT(*) FILTER (WHERE total_pendapatan < 1000000) as kurang_1,
COUNT(*) FILTER (WHERE total_pendapatan >= 1000000 AND total_pendapatan <= 3000000) as p1_3,
COUNT(*) FILTER (WHERE total_pendapatan > 3000000 AND total_pendapatan <= 5000000) as p3_5,
@@ -97,6 +216,8 @@ class DemografiPetaniController extends Controller
COUNT(*) FILTER (WHERE total_pendapatan > 10000000) as lebih_10
")->first();
$ratarataPenghasilan = (float) ($penghasilanBuckets->rata_rata ?? 0);
$penghasilan = [
'kurang_1' => $penghasilanBuckets->kurang_1,
'1_3' => $penghasilanBuckets->p1_3,
@@ -112,6 +233,7 @@ class DemografiPetaniController extends Controller
'total_komoditas' => $totalKomoditas,
'total_luas_lahan' => $totalLuasLahan,
'ratarata_luas_lahan' => $ratarataLuasLahan,
'ratarata_penghasilan' => $ratarataPenghasilan,
'kelompok_umur' => [
'kurang_30' => $umurCounts->kurang_30,
'30_39' => $umurCounts->umur_30_39,
@@ -120,31 +242,88 @@ class DemografiPetaniController extends Controller
'60_keatas' => $umurCounts->umur_60_keatas,
],
'pendidikan' => $pendidikan,
'jumlah_tanggungan' => $jumlahTanggungan,
'kelompok_tani' => $kelompokTani,
'desa_sasaran' => [
'total' => $desaSasaranRows->count(),
'ids' => $desaSasaranRows->pluck('desa_id')->values(),
'labels' => $desaSasaranRows->pluck('desa')->values(),
'series' => $desaSasaranRows->pluck('total')->map(fn ($v) => (int) $v)->values(),
],
'komoditas_utama' => [
'ids' => $komoditasUtama->pluck('id')->values(),
'labels' => $komoditasUtama->pluck('nama')->values(),
'series' => $komoditasUtama->pluck('petani_count')->map(fn ($v) => (int) $v)->values(),
],
'luas_lahan' => $luasLahan,
'status_kepemilikan_lahan' => [
'keys' => $statusKepemilikanLahanRows->pluck('status')->values(),
'labels' => $statusKepemilikanLahanRows
->pluck('status')
->map(fn ($status) => $this->statusLahanLabel((string) $status))
->values(),
'series' => $statusKepemilikanLahanRows->pluck('total')->map(fn ($v) => (int) $v)->values(),
],
'penghasilan' => $penghasilan,
];
$daftarPetani = Petani::with(['komoditas'])
->withSum('lahans as total_luas_lahan', 'luas_lahan')
->orderBy('id')
->paginate(10);
return compact('data');
}
return view('dashboard.demografi-petani', compact('data', 'daftarPetani'));
private function filteredPetaniIdsQuery(Request $request)
{
$query = Petani::query()
->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->whereHas('skor', function ($query) use ($request) {
match ($request->segmen) {
'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(function ($query) {
$query->whereNull('skor_kesiapan_adopsi')
->orWhere('skor_kesiapan_adopsi', '<', 1);
}),
default => null,
};
});
}
return $query->select('petani.id');
}
private function statusLahanLabel(string $status): string
{
return match ($status) {
'sendiri' => 'Milik Pribadi',
'sewa' => 'Sewa',
'adat' => 'Tanah Adat',
default => ucwords(str_replace('_', ' ', $status)),
};
}
public function data(Request $request)
{
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
$pendapatanSub = PendapatanKomoditas::query()
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
->whereColumn('sosial_ekonomi.petani_id', 'petani.id')
->selectRaw('SUM(pendapatan_komoditas.pendapatan)');
$query = Petani::with('komoditas')
->whereIn('petani.id', clone $filteredPetaniIds)
->withSum('lahans as total_luas_lahan', 'luas_lahan')
->addSelect(['total_pendapatan' => $pendapatanSub]);
@@ -187,6 +366,45 @@ class DemografiPetaniController extends Controller
}
}
if ($request->filled('jumlah_tanggungan')) {
$tanggunganQuery = SosialEkonomi::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->whereNotNull('jumlah_tanggungan')
->select('petani_id')
->distinct();
match ($request->jumlah_tanggungan) {
'0' => $tanggunganQuery->where('jumlah_tanggungan', 0),
'1_2' => $tanggunganQuery->whereBetween('jumlah_tanggungan', [1, 2]),
'3_4' => $tanggunganQuery->whereBetween('jumlah_tanggungan', [3, 4]),
'5_keatas' => $tanggunganQuery->where('jumlah_tanggungan', '>=', 5),
default => null,
};
$query->whereIn('petani.id', $tanggunganQuery);
}
if ($request->filled('kelompok_tani')) {
match ($request->kelompok_tani) {
'terdaftar' => $query->whereIn('petani.id', SosialEkonomi::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->where('terdaftar_kelompok_tani', 'ya')
->select('petani_id')
->distinct()),
'tidak_terdaftar' => $query->whereIn('petani.id', SosialEkonomi::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->where('terdaftar_kelompok_tani', 'tidak')
->select('petani_id')
->distinct()),
'belum_terisi' => $query->whereNotIn('petani.id', SosialEkonomi::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->whereIn('terdaftar_kelompok_tani', ['ya', 'tidak'])
->select('petani_id')
->distinct()),
default => null,
};
}
if ($request->filled('komoditas')) {
$query->where('komoditas_id', $request->komoditas);
}
@@ -213,15 +431,28 @@ class DemografiPetaniController extends Controller
}
if ($request->filled('luas_lahan')) {
$lahanQuery = Lahan::query()->select('petani_id')->groupBy('petani_id');
$lahanQuery = Lahan::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->select('petani_id')
->distinct();
match ($request->luas_lahan) {
'kurang_05' => $lahanQuery->havingRaw('SUM(luas_lahan) < 5000'),
'05_1' => $lahanQuery->havingRaw('SUM(luas_lahan) >= 5000 AND SUM(luas_lahan) <= 10000'),
'1_2' => $lahanQuery->havingRaw('SUM(luas_lahan) > 10000 AND SUM(luas_lahan) <= 20000'),
'lebih_2' => $lahanQuery->havingRaw('SUM(luas_lahan) > 20000'),
'kurang_05' => $lahanQuery->where('luas_lahan', '<', 0.5),
'05_1' => $lahanQuery->whereBetween('luas_lahan', [0.5, 1]),
'1_2' => $lahanQuery->where('luas_lahan', '>', 1)->where('luas_lahan', '<=', 2),
'lebih_2' => $lahanQuery->where('luas_lahan', '>', 2),
default => null,
};
$query->whereIn('id', $lahanQuery->pluck('petani_id'));
$query->whereIn('petani.id', $lahanQuery);
}
if ($request->filled('status_kepemilikan_lahan')) {
$query->whereIn('petani.id', Lahan::query()
->whereIn('petani_id', clone $filteredPetaniIds)
->where('status_kepemilikan', $request->status_kepemilikan_lahan)
->select('petani_id')
->distinct());
}
$daftarPetani = $query->orderBy('id')->paginate(10);
@@ -275,4 +506,4 @@ class DemografiPetaniController extends Controller
],
]);
}
}
}
@@ -5,21 +5,14 @@ namespace App\Http\Controllers\Web\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Database\Eloquent\Builder;
use App\Models\Pertanian\Petani;
use App\Models\Pertanian\Komoditas;
use App\Models\Pertanian\Lahan;
use App\Models\Pertanian\Tanaman;
use App\Models\Pertanian\SkorPetani;
use App\Models\Wilayah\Provinsi;
use App\Models\Wilayah\KabupatenKota;
use App\Models\Wilayah\Kecamatan;
use App\Models\Wilayah\DesaKelurahan;
class IndeksPetaniController extends Controller
{
@@ -36,13 +29,13 @@ class IndeksPetaniController extends Controller
];
}
private function petaniQuery(array $filters): \Illuminate\Database\Eloquent\Builder
private function petaniQuery(array $filters): Builder
{
$petaniQuery = Petani::query()
->join('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
->join('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
->join('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
->join('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
->whereNull('petani.deleted_at')
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
->when($filters['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
@@ -50,17 +43,21 @@ class IndeksPetaniController extends Controller
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
->when($filters['komoditas_id'], fn($q, $id) => $q->where('petani.komoditas_id', $id));
if ($filters['segmen']) {
$petaniQuery->whereHas('skor', fn (Builder $query) => $this->applySegmentFilter($query, $filters['segmen']));
}
return $petaniQuery;
}
private function skorPetaniQuery(array $filters, array $komoditas): \Illuminate\Database\Eloquent\Builder
private function skorPetaniQuery(array $filters, array $komoditas): Builder
{
$skor = SkorPetani::query()
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
->join('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
->join('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
->join('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
->join('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
->whereNull('petani.deleted_at')
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
->when($filters['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
@@ -68,33 +65,31 @@ class IndeksPetaniController extends Controller
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
->whereIn('petani.komoditas_id', $komoditas);
if ($filters['segmen']) {
$skor->where(function ($q) use ($filters) {
match ($filters['segmen']) {
'Penggerak' => $q->where('skor_kesiapan_adopsi', '>=', 4),
'Perintis' => $q->where(function($q) {
$q->where('skor_kesiapan_adopsi', '>=', 3)
->where('skor_kesiapan_adopsi', '<', 4);
}),
'Pemerhati' => $q->where(function($q) {
$q->where('skor_kesiapan_adopsi', '>=', 2)
->where('skor_kesiapan_adopsi', '<', 3);
}),
'Pemula' => $q->where(function($q) {
$q->where('skor_kesiapan_adopsi', '>=', 1)
->where('skor_kesiapan_adopsi', '<', 2);
}),
'Belum Lengkap' => $q->where(function ($q) {
$q->whereNull('skor_kesiapan_adopsi')
->orWhere('skor_kesiapan_adopsi', '<', 1);
}),
return $this->applySegmentFilter($skor, $filters['segmen']);
}
default => null,
};
});
private function applySegmentFilter(Builder $query, ?string $segment): Builder
{
$ranges = [
'Penggerak' => [4, null],
'Perintis' => [3, 4],
'Pemerhati' => [2, 3],
'Pemula' => [1, 2],
];
if (isset($ranges[$segment])) {
[$min, $max] = $ranges[$segment];
return $query->where('skor_kesiapan_adopsi', '>=', $min)
->when($max !== null, fn (Builder $query) => $query->where('skor_kesiapan_adopsi', '<', $max));
}
return $skor;
if ($segment === 'Belum Lengkap') {
$query->where(fn (Builder $query) => $query->whereNull('skor_kesiapan_adopsi')
->orWhere('skor_kesiapan_adopsi', '<', 1));
}
return $query;
}
public function index(Request $request)
@@ -103,18 +98,28 @@ class IndeksPetaniController extends Controller
abort(403);
}
return redirect()->route('dashboard.karakteristik-petani', array_merge($request->query(), [
'tab' => 'indeks-petani',
]));
}
public function contentData(Request $request): array
{
$filters = $this->filters($request);
$title = 'Dashboard Profil Petani';
$segments = $this->segments();
$colors = collect($segments)
->pluck('color')
->map(fn($c) => "var(--bs-{$c})")
->values();
$komoditas = Komoditas::where('is_utama', true)->get();
$petaniQuery = $this->petaniQuery($filters);
$komoditas = Komoditas::where('is_utama', true)->get(['id', 'nama']);
$petaniQuery = $this->petaniQuery(array_fill_keys(array_keys($filters), null));
$desaKelurahan = $petaniQuery->clone()
->selectRaw("
master_desa_kelurahan.id as desa_kelurahan_id,
master_desa_kelurahan.nama as desa_kelurahan,
master_desa_kelurahan.path as path,
master_kecamatan.id as kecamatan_id,
master_kecamatan.nama as kecamatan,
master_kabupaten_kota.id as kabupaten_id,
@@ -122,9 +127,15 @@ class IndeksPetaniController extends Controller
master_provinsi.id as provinsi_id,
master_provinsi.nama as provinsi
")
->whereNotNull('master_provinsi.id')
->distinct()
->get();
$provinsi = $desaKelurahan
->unique('provinsi_id')
->sortBy('provinsi')
->values();
$wilayah = $desaKelurahan
->groupBy('provinsi_id')
->map(function ($provinsi) {
@@ -147,7 +158,6 @@ class IndeksPetaniController extends Controller
return [
'id' => $desa->desa_kelurahan_id,
'nama' => $desa->desa_kelurahan,
'path' => $desa->path,
];
})->values(),
];
@@ -157,12 +167,15 @@ class IndeksPetaniController extends Controller
];
})->values();
return view('dashboard.indeks-petani', array_merge($filters,
return array_merge($filters,
compact(
'komoditas',
'desaKelurahan',
'provinsi',
'wilayah',
)));
'segments',
'colors',
));
}
public function summary(Request $request): JsonResponse
@@ -171,23 +184,21 @@ class IndeksPetaniController extends Controller
$petaniQuery = $this->petaniQuery($filters);
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$countKomoditas = $petaniQuery->clone()
->whereHas('komoditas', fn ($query) => $query->where('is_utama', true))
->distinct()->count('komoditas_id');
$counts = $petaniQuery->clone()
->selectRaw('COUNT(*) as petani')
->selectRaw('COUNT(DISTINCT petani.komoditas_id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as komoditas')
->selectRaw('COUNT(DISTINCT petani.desa_kelurahan_id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as desa')
->selectRaw('COUNT(DISTINCT master_kecamatan.id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as kecamatan')
->first();
$filteredPetaniIds = $petaniQuery
->clone()
->select('petani.id')
->distinct()
->pluck('petani.id');
$sumLahan = Lahan::has('petani')->whereIn('petani_id', $filteredPetaniIds)->whereIn('komoditas_id', $komoditas->pluck('id'))->sum('luas_lahan');
$avgLahan = Lahan::has('petani')->whereIn('petani_id', $filteredPetaniIds)->whereIn('komoditas_id', $komoditas->pluck('id'))->avg('luas_lahan');
$countDesa = $petaniQuery->clone()->whereIn('komoditas_id', $komoditas->pluck('id'))->distinct()->count('desa_kelurahan_id');
$countKecamatan = $petaniQuery->clone()->whereIn('komoditas_id', $komoditas->pluck('id'))->distinct()->count('master_kecamatan.id');
$filteredPetaniIds = $petaniQuery->clone()->select('petani.id');
$lahan = Lahan::query()
->whereIn('petani_id', $filteredPetaniIds)
->whereIn('komoditas_id', $komoditas->pluck('id'))
->selectRaw('COALESCE(SUM(luas_lahan), 0) as total, AVG(luas_lahan) as rata_rata')
->first();
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
$avgSkor = $skorQuery->clone()->avg('skor_kesiapan_adopsi');
@@ -205,12 +216,12 @@ class IndeksPetaniController extends Controller
}
return response()->json([
'petani' => $petaniQuery->clone()->count(),
'komoditas' => $countKomoditas,
'sum_lahan' => $sumLahan,
'avg_lahan' => $avgLahan,
'desa' => $countDesa,
'kecamatan' => $countKecamatan,
'petani' => (int) $counts->petani,
'komoditas' => (int) $counts->komoditas,
'sum_lahan' => (float) $lahan->total,
'avg_lahan' => (float) $lahan->rata_rata,
'desa' => (int) $counts->desa,
'kecamatan' => (int) $counts->kecamatan,
'skor' => $avgSkor,
'segmen' => $avgSegmen
]);
@@ -269,7 +280,7 @@ class IndeksPetaniController extends Controller
$case = $this->cases();
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
@@ -313,7 +324,7 @@ class IndeksPetaniController extends Controller
$case = $this->cases();
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
@@ -355,7 +366,7 @@ class IndeksPetaniController extends Controller
$filters = $this->filters($request);
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
@@ -380,7 +391,7 @@ class IndeksPetaniController extends Controller
$filters = $this->filters($request);
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
@@ -405,17 +416,8 @@ class IndeksPetaniController extends Controller
]);
}
public function map(Request $request): JsonResponse
private function villageData(array $filters, bool $includePath = true): \Illuminate\Support\Collection
{
$filters = $this->filters($request);
$segments = $this->segments();
$colors = collect($segments)
->pluck('color')
->map(fn($c) => "var(--bs-{$c})")
->values();
$segmentStyles = collect($segments)->keyBy('name');
$case = $this->cases();
$petaniQuery = $this->petaniQuery($filters);
@@ -424,7 +426,6 @@ class IndeksPetaniController extends Controller
->selectRaw("
master_desa_kelurahan.id as desa_kelurahan_id,
master_desa_kelurahan.nama as desa_kelurahan,
master_desa_kelurahan.path as path,
master_kecamatan.id as kecamatan_id,
master_kecamatan.nama as kecamatan,
master_kabupaten_kota.id as kabupaten_id,
@@ -432,11 +433,12 @@ class IndeksPetaniController extends Controller
master_provinsi.id as provinsi_id,
master_provinsi.nama as provinsi
")
->when($includePath, fn (Builder $query) => $query->addSelect('master_desa_kelurahan.path as path'))
->distinct()
->get();
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
@@ -464,17 +466,21 @@ class IndeksPetaniController extends Controller
->groupBy('petani.desa_kelurahan_id')
->get();
$segmenDominan = $mapSegmentasi
->groupBy('desa_kelurahan_id')
$segmentasiGroups = $mapSegmentasi->groupBy('desa_kelurahan_id');
$jumlahSegmenByDesa = $segmentasiGroups
->map(fn ($items) => $items->pluck('total', 'segmen'));
$segmenDominan = $segmentasiGroups
->map(function ($items) {
return $items->sortByDesc('total')->first();
return $items->reduce(fn ($largest, $item) => $largest === null || $item->total > $largest->total ? $item : $largest);
});
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan) {
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan, $jumlahSegmenByDesa) {
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
$item->segmen_dominan = $dominan?->segmen;
$item->jumlah_dominan = $dominan?->total;
$item->jumlah_segmen = $jumlahSegmenByDesa->get($item->desa_kelurahan_id, collect());
});
$segmentasiByDesa = $mapAvgSkorSegmentasi->keyBy('desa_kelurahan_id');
@@ -490,97 +496,22 @@ class IndeksPetaniController extends Controller
return $desa;
})->filter()->values();
return response()->json($mapDesaSegmentasi);
return $mapDesaSegmentasi;
}
public function map(Request $request): JsonResponse
{
return response()->json($this->villageData($this->filters($request)));
}
public function desa(Request $request): JsonResponse
{
$filters = $this->filters($request);
$segments = $this->segments();
$segmentStyles = collect($this->segments())->keyBy('name');
$mapDesaSegmentasi = $this->villageData($this->filters($request), false);
$colors = collect($segments)
->pluck('color')
->map(fn($c) => "var(--bs-{$c})")
->values();
$segmentStyles = collect($segments)->keyBy('name');
$case = $this->cases();
$petaniQuery = $this->petaniQuery($filters);
$desaKelurahan = $petaniQuery->clone()
->selectRaw("
master_desa_kelurahan.id as desa_kelurahan_id,
master_desa_kelurahan.nama as desa_kelurahan,
master_desa_kelurahan.path as path,
master_kecamatan.id as kecamatan_id,
master_kecamatan.nama as kecamatan,
master_kabupaten_kota.id as kabupaten_id,
master_kabupaten_kota.nama as kabupaten,
master_provinsi.id as provinsi_id,
master_provinsi.nama as provinsi
")
->distinct()
->get();
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
$mapSegmentasi = $skorQuery->clone()
->selectRaw("petani.desa_kelurahan_id,
$case AS segmen,
COUNT(petani.id) AS total
")
->groupBy('petani.desa_kelurahan_id')
->groupByRaw($case)
->get();
$mapAvgSkorSegmentasi = $skorQuery->clone()
->selectRaw("petani.desa_kelurahan_id,
COUNT(petani.id) AS jumlah_petani,
avg(skor_kesiapan_adopsi) AS skor,
CASE
WHEN AVG(skor_kesiapan_adopsi) >= 4 THEN 'Penggerak'
WHEN AVG(skor_kesiapan_adopsi) >= 3 THEN 'Perintis'
WHEN AVG(skor_kesiapan_adopsi) >= 2 THEN 'Pemerhati'
WHEN AVG(skor_kesiapan_adopsi) >= 1 THEN 'Pemula'
ELSE 'Belum Lengkap'
END AS segmen
")
->groupBy('petani.desa_kelurahan_id')
->get();
$segmenDominan = $mapSegmentasi
->groupBy('desa_kelurahan_id')
->map(function ($items) {
return $items->sortByDesc('total')->first();
});
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan) {
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
$item->segmen_dominan = $dominan?->segmen;
$item->jumlah_dominan = $dominan?->total;
});
$segmentasiByDesa = $mapAvgSkorSegmentasi->keyBy('desa_kelurahan_id');
$desaLookup = $desaKelurahan->keyBy('desa_kelurahan_id');
$mapDesaSegmentasi = $segmentasiByDesa->map(function ($segmentasi, $desaId) use ($desaLookup) {
$desa = $desaLookup->get($desaId);
if (!$desa) {
return null;
}
$desa->segmentasi = $segmentasi;
return $desa;
})->filter()->values();
$rows = $mapDesaSegmentasi->map(function ($desa) use ($segmentStyles) {
$segmentNames = ['Pemula', 'Perintis', 'Pemerhati', 'Penggerak', 'Belum Lengkap'];
$rows = $mapDesaSegmentasi->map(function ($desa) use ($segmentStyles, $segmentNames) {
$total = (int) $desa->segmentasi->jumlah_petani;
$style = $segmentStyles[$desa->segmentasi->segmen] ?? ['color' => 'secondary'];
$styleDominan = $segmentStyles[$desa->segmentasi->segmen_dominan] ?? ['color' => 'secondary'];
@@ -594,6 +525,16 @@ class IndeksPetaniController extends Controller
'jumlah_dominan' => $desa->segmentasi->jumlah_dominan,
'color' => $style['color'],
'color_dominan' => $styleDominan['color'],
'segments' => collect($segmentNames)
->mapWithKeys(function ($name) use ($desa, $segmentStyles, $total) {
$count = (int) $desa->segmentasi->jumlah_segmen->get($name, 0);
return [strtolower(str_replace(' ', '_', $name)) => [
'count' => $count,
'percentage' => $total > 0 ? round($count / $total * 100, 2) : 0,
'color' => $segmentStyles[$name]['color'] ?? 'secondary',
]];
})->all(),
];
});
@@ -605,30 +546,21 @@ class IndeksPetaniController extends Controller
$filters = $this->filters($request);
$segments = $this->segments();
$colors = collect($segments)
->pluck('color')
->map(fn($c) => "var(--bs-{$c})")
->values();
$segmentStyles = collect($segments)->keyBy('name');
$petaniQuery = $this->petaniQuery($filters);
$komoditas = Komoditas::where('is_utama', true)
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
$daftarPetani = $petaniQuery->clone()
->select('petani.*')
->with(['desaKelurahan', 'komoditas', 'skor'])
->withSum('lahans as total_luas_lahan', 'luas_lahan')
->whereIn('komoditas_id', $komoditas->pluck('id'))
->get();
$data = $daftarPetani
->filter(function ($item) use ($filters) {
if (!$filters['segmen']) {
return true;
}
$segmen = $item->skor ? $item->segmen() : 'Belum Lengkap';
return $segmen === $filters['segmen'];
})
->map(function ($item) use ($segmentStyles) {
$segmen = $item->skor
? $item->segmen()
@@ -642,7 +574,7 @@ class IndeksPetaniController extends Controller
'desa' => $item->desaKelurahan?->nama,
'desa_kelurahan_id' => $item->desa_kelurahan_id,
'komoditas' => $item->komoditas?->nama,
'luas_lahan' => $item->lahans?->sum('luas_lahan') ?? 0,
'luas_lahan' => (float) ($item->total_luas_lahan ?? 0),
'skor' => $item->skor
? $item->skor->skor_kesiapan_adopsi
: null,
@@ -655,4 +587,4 @@ class IndeksPetaniController extends Controller
return response()->json($data);
}
}
}
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Web\Dashboard;
use App\Enums\KeparahanSerangan;
use App\Enums\PenyakitTanaman;
use App\Enums\PraktikBudidaya\KondisiPengelolaanAir;
use App\Enums\PraktikBudidaya\PemberianPupuk;
use App\Enums\SeranganHama;
use App\Enums\SosialEkonomi\StatusKepemilikanLahanSosialEkonomi;
@@ -13,12 +14,14 @@ use App\Models\Pertanian\Lahan;
use App\Models\Pertanian\Petani;
use App\Models\Pertanian\PraktikBudidaya;
use App\Models\Pertanian\Produksi;
use App\Models\Pertanian\SurveyPanen;
use App\Models\Pertanian\SkorPetani;
use App\Models\Pertanian\SosialEkonomi;
use App\Models\Pertanian\SurveyProduktivitas;
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;
@@ -95,11 +98,180 @@ class IndikatorPertanianController extends Controller
private function dashboardPayload(array $filters): array
{
return [
'overviewData' => $this->overviewData($filters),
'landProfileData' => $this->landProfileData($filters),
'summaryData' => $this->summaryData($filters),
'chartData' => $this->chartData($filters),
];
}
private function overviewData(array $filters): array
{
$totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count();
$totalLahan = $this->applyLahanFilters(Lahan::query(), $filters)->count();
$totalLuasLahan = (float) $this->applyLahanFilters(Lahan::has('petani'), $filters)->sum('luas_lahan');
$jumlahDesa = $this->applyLahanFilters(Lahan::query(), $filters)
->whereNotNull('desa_kelurahan_id')
->distinct('desa_kelurahan_id')
->count('desa_kelurahan_id');
$jumlahKecamatan = $this->applyLahanFilters(Lahan::query(), $filters)
->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id')
->whereNotNull('d.kecamatan_id')
->distinct('d.kecamatan_id')
->count('d.kecamatan_id');
$totalProduksiKg = $this->totalProduksiKg($filters);
$rataRataProduktivitas = $totalLuasLahan > 0 ? $totalProduksiKg / $totalLuasLahan : 0;
$tanamanProduktif = $this->tanamanProduktifCount($filters);
$batchPanen = $this->batchPanenCount($filters);
return [
'kpiCards' => [
[
'value' => number_format($totalPetani, 0, ',', '.'),
'label' => 'Jumlah Petani',
'description' => 'Petani yang tercakup dalam filter aktif',
'icon' => 'tabler-users',
'variant' => 'primary',
],
[
'value' => number_format($totalLahan, 0, ',', '.'),
'label' => 'Jumlah Kebun/Lahan',
'description' => 'Kebun atau lahan terdata',
'icon' => 'tabler-trees',
'variant' => 'success',
],
[
'value' => number_format($totalLuasLahan, 2, ',', '.').' ha',
'label' => 'Luas Lahan',
'description' => 'Akumulasi luas lahan terdata',
'icon' => 'tabler-ruler-measure',
'variant' => 'warning',
],
[
'value' => number_format($jumlahDesa, 0, ',', '.').' desa',
'label' => 'Desa/Kecamatan',
'description' => number_format($jumlahKecamatan, 0, ',', '.').' kecamatan tercakup',
'icon' => 'tabler-map-pin',
'variant' => 'info',
],
[
'value' => number_format($totalProduksiKg, 0, ',', '.').' kg',
'label' => 'Total Produksi',
'description' => $this->productionSummaryDescription($filters),
'icon' => 'tabler-wheat',
'variant' => 'primary',
],
[
'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha',
'label' => 'Produktivitas Rata-rata',
'description' => 'Berdasarkan produksi dan luas lahan terdata',
'icon' => 'tabler-chart-bar',
'variant' => 'success',
],
[
'value' => number_format($tanamanProduktif, 0, ',', '.'),
'label' => 'Tanaman Produktif',
'description' => 'Tanaman dengan kondisi baik',
'icon' => 'tabler-plant-2',
'variant' => 'warning',
],
[
'value' => number_format($batchPanen, 0, ',', '.'),
'label' => 'Batch Panen Tercatat',
'description' => 'Catatan produksi dan survey panen',
'icon' => 'tabler-basket',
'variant' => 'info',
],
],
'commodityFocus' => $this->commodityFocusData($filters),
'map' => [
'features' => $this->farmMapFeatures($filters),
'levels' => $this->mapLevelData($filters),
],
];
}
private function landProfileData(array $filters): array
{
$totalTanaman = (int) $this->applyLahanFilters(Lahan::query(), $filters)->sum('jumlah_tanaman');
$rataRataUmurTanaman = (float) $this->applyLahanFilters(Lahan::query(), $filters)
->whereNotNull('umur_tanaman')
->avg('umur_tanaman');
$komoditasCount = (int) $this->applyLahanFilters(Lahan::query(), $filters)
->whereNotNull('komoditas_id')
->distinct('komoditas_id')
->count('komoditas_id');
$statusLahanCount = (int) $this->applyLahanFilters(Lahan::query(), $filters)
->whereNotNull('status_kepemilikan')
->distinct('status_kepemilikan')
->count('status_kepemilikan');
$tanamanPenaung = $this->shadowTreeSummary($filters);
$tanamanPendamping = $this->companionPlantSummary($filters);
$diversityAverage = $this->plantDiversityAverage($filters);
return [
'conditionCards' => [
[
'value' => number_format($komoditasCount, 0, ',', '.'),
'label' => 'Luas Lahan per Komoditas',
'description' => 'Komoditas dengan luas lahan terdata',
'icon' => 'tabler-chart-treemap',
'variant' => 'primary',
],
[
'value' => number_format($totalTanaman, 0, ',', '.'),
'label' => 'Jumlah Tanaman',
'description' => 'Akumulasi jumlah tanaman pada lahan',
'icon' => 'tabler-trees',
'variant' => 'success',
],
[
'value' => number_format($rataRataUmurTanaman, 1, ',', '.').' tahun',
'label' => 'Umur Tanaman',
'description' => 'Rata-rata umur tanaman terdata',
'icon' => 'tabler-calendar-stats',
'variant' => 'warning',
],
[
'value' => number_format($statusLahanCount, 0, ',', '.'),
'label' => 'Status Lahan',
'description' => 'Jumlah kategori status kepemilikan lahan',
'icon' => 'tabler-certificate',
'variant' => 'info',
],
],
'agroecologyItems' => [
['label' => 'Sumber Air Lahan', 'value' => $this->sourceWaterSummary($filters), 'status' => 'available'],
['label' => 'Pengelolaan Air & Irigasi', 'value' => $this->waterManagementSummary($filters), 'status' => 'available'],
['label' => 'Kebutuhan Air per Musim', 'value' => $this->seasonalWaterNeedSummary($filters), 'status' => 'available'],
['label' => 'Praktik Konservasi Tanah', 'value' => $this->soilConservationSummary($filters), 'status' => 'available'],
],
'agroforestryCards' => [
[
'value' => number_format($tanamanPenaung['total'], 0, ',', '.'),
'label' => 'Jumlah Tanaman Penaung',
'description' => $tanamanPenaung['types'].' jenis tanaman penaung',
'icon' => 'tabler-leaf',
'variant' => 'success',
],
[
'value' => number_format($tanamanPendamping['types'], 0, ',', '.'),
'label' => 'Jenis Tanaman Pendamping',
'description' => number_format($tanamanPendamping['total'], 0, ',', '.').' entri tanaman pendamping',
'icon' => 'tabler-flower',
'variant' => 'warning',
],
[
'value' => number_format(round($diversityAverage), 0, ',', '.'),
'label' => 'Tingkat Campuran Tanaman',
'description' => 'Rata-rata kombinasi tanaman utama, pendamping, dan penaung',
'icon' => 'tabler-binary-tree',
'variant' => 'primary',
],
],
];
}
private function summaryData(array $filters): array
{
$totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count();
@@ -166,6 +338,10 @@ class IndikatorPertanianController extends Controller
'statusPanen' => $this->statusPanenChart($filters),
'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($filters, 'ha_tahun'),
'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($filters, 'pohon_tahun'),
'luasLahanPerKomoditas' => $this->luasLahanPerKomoditasChart($filters),
'jumlahTanamanPerKomoditas' => $this->jumlahTanamanPerKomoditasChart($filters),
'umurTanaman' => $this->umurTanamanChart($filters),
'sumberAir' => $this->sumberAirChart($filters),
];
}
@@ -355,6 +531,82 @@ class IndikatorPertanianController extends Controller
];
}
private function luasLahanPerKomoditasChart(array $filters): array
{
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
->whereNotNull('map_lahan.luas_lahan')
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
->selectRaw('SUM(map_lahan.luas_lahan) as total')
->groupBy('k.id', 'k.nama')
->havingRaw('SUM(map_lahan.luas_lahan) > 0')
->orderByDesc('total')
->get();
return [
'labels' => $rows->pluck('label')->values()->all(),
'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(),
];
}
private function jumlahTanamanPerKomoditasChart(array $filters): array
{
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
->whereNotNull('map_lahan.jumlah_tanaman')
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
->selectRaw('SUM(map_lahan.jumlah_tanaman) as total')
->groupBy('k.id', 'k.nama')
->havingRaw('SUM(map_lahan.jumlah_tanaman) > 0')
->orderByDesc('total')
->get();
return [
'labels' => $rows->pluck('label')->values()->all(),
'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(),
];
}
private function umurTanamanChart(array $filters): array
{
$bucketOrder = ['< 3 tahun', '3-7 tahun', '8-15 tahun', '> 15 tahun'];
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
->whereNotNull('umur_tanaman')
->pluck('umur_tanaman')
->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()
->sortBy(fn ($value, $label) => array_search($label, $bucketOrder, true));
return [
'labels' => $rows->keys()->values()->all(),
'series' => $rows->values()->map(fn ($value) => (int) $value)->values()->all(),
];
}
private function sumberAirChart(array $filters): array
{
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
->get(['sumber_air'])
->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->sumber_air))
->filter()
->countBy()
->sortDesc();
return [
'labels' => $rows->keys()->values()->all(),
'series' => $rows->values()->map(fn ($value) => (int) $value)->values()->all(),
];
}
private function kondisiTanamanChart(array $filters): array
{
$rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
@@ -572,6 +824,415 @@ class IndikatorPertanianController extends Controller
];
}
private function tanamanProduktifCount(array $filters): int
{
return (int) $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
->whereNull('map_tanaman.deleted_at')
->where('map_tanaman.kondisi_tanaman', 'baik')
->count('map_tanaman.id');
}
private function batchPanenCount(array $filters): int
{
$produksiCount = $this->applyProduksiFilters(Produksi::query(), $filters)->count();
$surveyPanenCount = $this->applySurveyPanenFilters(SurveyPanen::query(), $filters)->count();
return (int) $produksiCount + (int) $surveyPanenCount;
}
private function productionSummaryDescription(array $filters): string
{
$chart = $this->produksiPerKomoditasChart($filters);
if (empty($chart['series'])) {
return 'Belum ada produksi aktual per komoditas';
}
return collect($chart['labels'])
->map(fn ($label, $index) => $label.': '.number_format((float) ($chart['series'][$index] ?? 0), 0, ',', '.').' kg')
->take(3)
->implode(' | ');
}
private function commodityFocusData(array $filters): array
{
$focusCommodities = ['Kopi', 'Kakao', 'Vanili', 'Kemiri'];
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
->whereIn(DB::raw('LOWER(k.nama)'), collect($focusCommodities)->map(fn ($name) => strtolower($name))->all())
->selectRaw('k.nama as label')
->selectRaw('COUNT(map_lahan.id) as total')
->groupBy('k.nama')
->get()
->mapWithKeys(fn ($row) => [strtolower((string) $row->label) => (int) $row->total]);
return collect($focusCommodities)
->map(fn ($name) => [
'label' => $name,
'total' => $rows[strtolower($name)] ?? 0,
])
->values()
->all();
}
private function farmMapFeatures(array $filters): array
{
return $this->applyLahanFilters(
Lahan::query()->with(['desaKelurahan.kecamatan', 'komoditas']),
$filters
)
->where(function ($query) {
$query
->whereNotNull('lat')
->orWhereNotNull('long')
->orWhereNotNull('path');
})
->limit(250)
->get()
->map(fn (Lahan $lahan) => [
'id' => $lahan->id,
'name' => $lahan->nama,
'commodity' => $lahan->komoditas?->nama ?? 'Belum ditentukan',
'area' => (float) $lahan->luas_lahan,
'village' => $lahan->desaKelurahan?->nama,
'district' => $lahan->desaKelurahan?->kecamatan?->nama,
'lat' => is_numeric($lahan->lat) ? (float) $lahan->lat : null,
'lng' => is_numeric($lahan->long) ? (float) $lahan->long : null,
'path' => is_array($lahan->path) ? $lahan->path : null,
])
->values()
->all();
}
private function mapLevelData(array $filters): array
{
$lahanSummaryByDesa = $this->applyLahanFilters(Lahan::query(), $filters)
->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();
$desaIds = $lahanSummaryByDesa->pluck('desa_kelurahan_id')->filter()->unique()->values();
if ($filters['desa'] !== 'all') {
$desaIds->push($filters['desa']);
}
$desaList = DesaKelurahan::query()
->select('id', 'nama', 'path', 'kecamatan_id')
->when($desaIds->isNotEmpty(), fn ($query) => $query->whereIn('id', $desaIds->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');
$geoProvinsi = [];
$geoKabupaten = [];
$geoKecamatan = [];
$geoDesa = [];
$geoLahan = [];
foreach ($lahanSummaryByDesa as $summary) {
$desa = $desaList->get($summary->desa_kelurahan_id);
$kec = $desa?->kecamatan;
$kab = $kec?->kabupatenKota;
$prov = $kab?->provinsi;
$totalLahan = (int) $summary->total_lahan;
$totalLuas = (float) $summary->total_luas;
if ($prov && $prov->path) {
$this->accumulateGeoLevel($geoProvinsi, $prov->id, null, $prov->nama, $prov->path, $totalLahan, $totalLuas);
}
if ($kab && $kab->path) {
$this->accumulateGeoLevel($geoKabupaten, $kab->id, $prov?->id, $kab->nama, $kab->path, $totalLahan, $totalLuas);
}
if ($kec && $kec->path) {
$this->accumulateGeoLevel($geoKecamatan, $kec->id, $kab?->id, $kec->nama, $kec->path, $totalLahan, $totalLuas);
}
if ($desa && $desa->path) {
$this->accumulateGeoLevel($geoDesa, $desa->id, $kec?->id, $desa->nama, $desa->path, $totalLahan, $totalLuas);
}
}
$this->ensureSelectedRegionOnMap($filters, $geoProvinsi, $geoKabupaten, $geoKecamatan, $geoDesa);
$mapLahan = $this->applyLahanFilters(
Lahan::query()->with([
'profile:id,nama',
'petani:id,nama',
'komoditas:id,nama',
'tanaman:id,lahan_id,komoditas_id',
'tanaman.komoditas:id,nama',
]),
$filters
)
->whereNotNull('path')
->orderByDesc('updated_at')
->limit(500)
->get();
foreach ($mapLahan as $lahan) {
if (! $this->hasMapPolygon($lahan->path)) {
continue;
}
$tanaman = $lahan->tanaman
->map(fn ($tanaman) => $tanaman->komoditas?->nama)
->filter()
->unique()
->values();
if ($tanaman->isEmpty() && $lahan->komoditas?->nama) {
$tanaman = collect([$lahan->komoditas->nama]);
}
$geoLahan[] = [
'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,
'tanaman' => $tanaman,
];
}
return [
'provinsi' => array_values($geoProvinsi),
'kabupaten' => array_values($geoKabupaten),
'kecamatan' => array_values($geoKecamatan),
'desa' => array_values($geoDesa),
'lahan' => $geoLahan,
];
}
private function accumulateGeoLevel(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 ensureSelectedRegionOnMap(array $filters, array &$geoProvinsi, array &$geoKabupaten, array &$geoKecamatan, array &$geoDesa): void
{
$kabupaten = $filters['kabupaten'] !== 'all'
? KabupatenKota::query()->with('provinsi:id,nama,path')->find($filters['kabupaten'])
: null;
if ($kabupaten && $kabupaten->path && ! isset($geoKabupaten[$kabupaten->id])) {
$this->accumulateGeoLevel($geoKabupaten, $kabupaten->id, $kabupaten->provinsi_id, $kabupaten->nama, $kabupaten->path, 0, 0);
}
if ($kabupaten?->provinsi && $kabupaten->provinsi->path && ! isset($geoProvinsi[$kabupaten->provinsi->id])) {
$this->accumulateGeoLevel($geoProvinsi, $kabupaten->provinsi->id, null, $kabupaten->provinsi->nama, $kabupaten->provinsi->path, 0, 0);
}
if ($filters['kecamatan'] !== 'all') {
$kecamatan = Kecamatan::query()->find($filters['kecamatan']);
if ($kecamatan && $kecamatan->path && ! isset($geoKecamatan[$kecamatan->id])) {
$this->accumulateGeoLevel($geoKecamatan, $kecamatan->id, $kecamatan->kabupaten_kota_id, $kecamatan->nama, $kecamatan->path, 0, 0);
}
}
if ($filters['desa'] !== 'all') {
$desa = DesaKelurahan::query()->find($filters['desa']);
if ($desa && $desa->path && ! isset($geoDesa[$desa->id])) {
$this->accumulateGeoLevel($geoDesa, $desa->id, $desa->kecamatan_id, $desa->nama, $desa->path, 0, 0);
}
}
}
private function hasMapPolygon(mixed $path): bool
{
if (is_array($path)) {
return ! empty($path);
}
if (! is_string($path) || blank($path) || strtolower($path) === 'null') {
return false;
}
$decoded = json_decode($path, true);
return json_last_error() === JSON_ERROR_NONE && ! empty($decoded) && $decoded !== 'null';
}
private function shadowTreeSummary(array $filters): array
{
$items = $this->applyLahanFilters(Lahan::query(), $filters)
->get(['tanaman_penaung'])
->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->tanaman_penaung))
->filter()
->values();
return [
'total' => $items->count(),
'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(),
];
}
private function companionPlantSummary(array $filters): array
{
$items = $this->applyLahanFilters(Lahan::query(), $filters)
->get(['id', 'komoditas_lain'])
->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->komoditas_lain))
->filter()
->values();
return [
'total' => $items->count(),
'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(),
];
}
private function plantDiversityAverage(array $filters): float
{
$lahan = $this->applyLahanFilters(Lahan::query(), $filters)
->get(['komoditas_id', 'komoditas_lain', 'tanaman_penaung']);
if ($lahan->isEmpty()) {
return 0;
}
return (float) $lahan->avg(function (Lahan $item) {
$total = $item->komoditas_id ? 1 : 0;
$total += count($this->arrayValues($item->komoditas_lain));
$total += count($this->arrayValues($item->tanaman_penaung));
return $total;
});
}
private function sourceWaterSummary(array $filters): string
{
$chart = $this->sumberAirChart($filters);
if (empty($chart['labels'])) {
return 'Belum ada data';
}
return collect($chart['labels'])
->map(fn ($label, $index) => $label.' ('.number_format((int) ($chart['series'][$index] ?? 0), 0, ',', '.').')')
->take(2)
->implode(', ');
}
private function waterManagementSummary(array $filters): string
{
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
->whereNotNull('kondisi_pengelolaan_air')
->pluck('kondisi_pengelolaan_air')
->map(fn ($value) => $this->waterManagementShortLabel((int) $value))
->countBy()
->sortDesc();
return $this->topCountSummary($rows);
}
private function seasonalWaterNeedSummary(array $filters): string
{
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
->whereNotNull('kebutuhan_air_permusim')
->pluck('kebutuhan_air_permusim')
->map(fn ($value) => $this->shortEnumLabel((string) $value))
->countBy()
->sortDesc();
return $this->topCountSummary($rows);
}
private function soilConservationSummary(array $filters): string
{
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
->get(['kebiasaan_konservasi_tanah'])
->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->kebiasaan_konservasi_tanah))
->filter()
->countBy()
->sortDesc();
return $this->topCountSummary($rows);
}
private function soilConservationScoreSummary(array $filters): string
{
$average = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
->whereNotNull('nilai_konservasi_tanah')
->avg('nilai_konservasi_tanah');
if ($average === null) {
return 'Belum ada data';
}
return 'Rata-rata '.number_format((float) $average, 1, ',', '.');
}
private function topCountSummary($rows): string
{
if ($rows->isEmpty()) {
return 'Belum ada data';
}
return $rows
->map(fn ($count, $label) => $label.' ('.number_format((int) $count, 0, ',', '.').')')
->take(2)
->implode(', ');
}
private function waterManagementShortLabel(int $value): string
{
return match (KondisiPengelolaanAir::tryFrom($value)) {
KondisiPengelolaanAir::KURANG_TIDAK_ADA_UPAYA => 'Kurang, tanpa upaya',
KondisiPengelolaanAir::KURANG_TIDAK_SUDAH_BERUPAYA => 'Kurang, ada upaya',
KondisiPengelolaanAir::CUKUP_DARI_HUJAN => 'Cukup dari hujan',
KondisiPengelolaanAir::CUKUP_IRIGASI_PINJAM => 'Cukup, irigasi pinjam',
KondisiPengelolaanAir::CUKUP_IRIGASI_SENDIRI => 'Cukup, irigasi sendiri',
default => 'Belum ditentukan',
};
}
private function arrayValues(mixed $value): array
{
if (is_array($value)) {
return collect($value)
->flatten()
->filter(fn ($item) => filled($item))
->map(fn ($item) => is_scalar($item) ? (string) $item : json_encode($item))
->values()
->all();
}
if (is_string($value) && filled($value) && strtolower($value) !== 'null') {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $this->arrayValues($decoded);
}
return [$value];
}
return [];
}
private function chartFromRows($rows, callable $labelResolver): array
{
return [
@@ -676,6 +1337,13 @@ class IndikatorPertanianController extends Controller
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']));
}
private function applySurveyPanenFilters($query, array $filters)
{
return $query->whereHas('petani', function ($petani) use ($filters) {
$this->applyPetaniFilters($petani, $filters);
});
}
private function applyTanamanTableFilters($query, array $filters)
{
return $query
File diff suppressed because it is too large Load Diff
@@ -489,34 +489,34 @@ class SurveyPetaniController extends Controller
{
return [
[
'color' => '#28a745',
'bg' => 'rgba(40,167,69,.15)',
'tx' => '#28a745',
'color' => '#6FD68F',
'bg' => 'rgba(111,214,143,.24)',
'tx' => '#6FD68F',
],
[
'color' => '#007bff',
'bg' => 'rgba(0,123,255,.15)',
'tx' => '#007bff',
'color' => '#5FC7D6',
'bg' => 'rgba(95,199,214,.24)',
'tx' => '#5FC7D6',
],
[
'color' => '#fd7e14',
'bg' => 'rgba(253,126,20,.15)',
'tx' => '#fd7e14',
'color' => '#FFB46F',
'bg' => 'rgba(255,180,111,.26)',
'tx' => '#FFB46F',
],
[
'color' => '#dc3545',
'bg' => 'rgba(220,53,69,.15)',
'tx' => '#dc3545',
'color' => '#F47C86',
'bg' => 'rgba(244,124,134,.26)',
'tx' => '#F47C86',
],
[
'color' => '#6f42c1',
'bg' => 'rgba(111,66,193,.15)',
'tx' => '#6f42c1',
'color' => '#9D8EF0',
'bg' => 'rgba(157,142,240,.26)',
'tx' => '#9D8EF0',
],
[
'color' => '#20c997',
'bg' => 'rgba(32,201,151,.15)',
'tx' => '#20c997',
'color' => '#65D8BD',
'bg' => 'rgba(101,216,189,.26)',
'tx' => '#65D8BD',
],
];
}
@@ -118,7 +118,8 @@ class AksesPasarController extends Controller
'skorPengalamanBerhentiValues' => [
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
]
],
'sumberInfoLainnyaValue' => 'Lainnya',
];
}
@@ -128,6 +129,17 @@ class AksesPasarController extends Controller
return $request->validate([
'petani_id' => ['required', 'exists:petani,id'],
'tujuan_penjualan' => ['nullable', 'array'],
'tujuan_penjualan.*' => ['string'],
'tujuan_penjualan_lainnya' => [
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('tujuan_penjualan', []), true)),
'nullable', 'string', 'max:255'
],
'sifat_hubungan_bisnis' => ['nullable', 'string', 'max:255'],
'bentuk_penjualan' => ['nullable', 'string', 'max:255'],
'frekuensi_penjualan' => ['nullable', 'string', 'max:255'],
'skor_pengalaman' => ['nullable', 'integer', Rule::in(SkorPengalaman::values())],
'alasan_berhenti' => [
Rule::requiredIf(fn () => in_array((int)$request->input('skor_pengalaman'), [
@@ -143,6 +155,14 @@ class AksesPasarController extends Controller
Rule::requiredIf(fn () => in_array(SaluranTerdahulu::LAINNYA->value, $request->input('saluran_terdahulu', []), true)),
'nullable', 'string', 'max:255'
],
'tahu_harga_sebelumnya' => ['nullable', 'string', 'max:255'],
'sumber_info_harga' => ['nullable', 'array'],
'sumber_info_harga.*' => ['string'],
'sumber_info_harga_lainnya' => [
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('sumber_info_harga', []), true)),
'nullable', 'string', 'max:255'
],
'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())],
'proses_fermentasi' => [
@@ -168,6 +188,7 @@ class AksesPasarController extends Controller
], [
'petani_id.required' => 'Petani harus dipilih.',
'petani_id.exists' => 'Petani tidak ditemukan.',
'tujuan_penjualan_lainnya.required' => 'Keterangan saluran penjualan lainnya harus diisi.',
'skor_pengalaman.integer' => 'Skor pengalaman harus berupa angka.',
'skor_pengalaman.in' => 'Skor pengalaman tidak valid.',
'alasan_berhenti.required' => 'Alasan berhenti harus diisi.',
@@ -204,6 +225,10 @@ class AksesPasarController extends Controller
{
$payload = collect($validated)->except(['harga_jual'])->all();
if (!in_array('Lainnya', $payload['tujuan_penjualan'] ?? [], true)) {
$payload['tujuan_penjualan_lainnya'] = null;
}
if (!in_array((int)($payload['skor_pengalaman'] ?? 0), [
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
@@ -215,6 +240,10 @@ class AksesPasarController extends Controller
$payload['saluran_terdahulu_lainnya'] = null;
}
if (!in_array('Lainnya', $payload['sumber_info_harga'] ?? [], true)) {
$payload['sumber_info_harga_lainnya'] = null;
}
if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) {
$payload['proses_fermentasi'] = null;
$payload['model_fermentasi_memungkinkan'] = null;
@@ -223,7 +252,6 @@ class AksesPasarController extends Controller
return $payload;
}
private function syncHargaJual($petaniId, array $hargaJualData, $createdBy, $updatedBy = null): void
{
HargaJualPanen::where('petani_id', $petaniId)->delete();
@@ -279,4 +307,4 @@ class AksesPasarController extends Controller
return is_null($digits) ? '' : number_format((int) $digits, 0, ',', '.');
}
}
}
@@ -12,6 +12,8 @@ use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use App\Models\Wilayah\Provinsi;
use App\Models\Wilayah\KabupatenKota;
use App\Models\Wilayah\Kecamatan;
use App\Models\Pertanian\Petani;
use App\Models\Wilayah\DesaKelurahan;
use App\Models\Pertanian\Komoditas;
@@ -35,6 +37,31 @@ class PetaniController extends Controller
}
$provinsi = Provinsi::select('id', 'kode', 'nama')->orderBy('kode', 'asc')->get();
$userRole = auth()->user()->role->value;
$showFasilitatorColumn = in_array($userRole, ['super_admin', 'admin'], true);
$defaultKabupaten = KabupatenKota::select('id', 'nama')
->whereRaw('lower(nama) like ?', ['%ngada%'])
->orderBy('nama')
->first();
$selectedKabupatenId = $request->query('kabupaten_kota_id', $defaultKabupaten?->id);
$allowedSorts = ['nama_asc', 'nama_desc', 'luas_lahan_desc', 'luas_lahan_asc'];
$requestedSort = $request->query('sort_by')
?? match ($request->query('sort_nama')) {
'asc' => 'nama_asc',
'desc' => 'nama_desc',
default => null,
};
$filters = [
'search' => $request->query('search'),
'komoditas_id' => $request->query('komoditas_id'),
'kabupaten_kota_id' => $selectedKabupatenId,
'kecamatan_id' => $request->query('kecamatan_id'),
'desa_kelurahan_id' => $request->query('desa_kelurahan_id'),
'sort_by' => in_array($requestedSort, $allowedSorts, true)
? $requestedSort
: null,
];
$query = Petani::with([
'desaKelurahan:id,nama,kecamatan_id',
@@ -43,29 +70,114 @@ class PetaniController extends Controller
'desaKelurahan.kecamatan.kabupatenKota.provinsi:id,nama',
'komoditas:id,nama',
'skor:id,petani_id,skor_kesiapan_adopsi',
'creator:id,name,role',
])
->orderBy('created_at', 'desc');
->withSum('lahans as total_luas_lahan', 'luas_lahan');
if ($request->search) {
$query->where(function ($query) use ($request) {
$query->where('nama', 'like', '%' . $request->search . '%')
->orWhere('ktp', 'like', '%' . $request->search . '%');
$search = $request->filled('search') ? Str::lower($request->search) : null;
if ($search) {
$query->where(function ($query) use ($search) {
$query->whereRaw('lower(nama) like ?', ["%{$search}%"])
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
});
}
if (auth()->user()->role->value === 'fasilitator') {
if ($request->filled('komoditas_id')) {
$query->where('komoditas_id', $request->komoditas_id);
}
if ($filters['kabupaten_kota_id']) {
$query->whereHas('desaKelurahan.kecamatan', function ($query) use ($filters) {
$query->where('kabupaten_kota_id', $filters['kabupaten_kota_id']);
});
}
if ($request->filled('kecamatan_id')) {
$query->whereHas('desaKelurahan', function ($query) use ($request) {
$query->where('kecamatan_id', $request->kecamatan_id);
});
}
if ($request->filled('desa_kelurahan_id')) {
$query->where('desa_kelurahan_id', $request->desa_kelurahan_id);
}
if ($userRole === 'fasilitator') {
$query->where('created_by', auth()->user()->id);
}
$totalPetani = (clone $query)->count();
$totalPetaniLakiLaki = (clone $query)->where('jenis_kelamin', 1)->count();
$totalPetaniPerempuan = (clone $query)->where('jenis_kelamin', 0)->count();
match ($filters['sort_by']) {
'nama_asc' => $query->orderBy('nama', 'asc')->orderBy('created_at', 'desc'),
'nama_desc' => $query->orderBy('nama', 'desc')->orderBy('created_at', 'desc'),
'luas_lahan_desc' => $query->orderByDesc(
Lahan::selectRaw('COALESCE(SUM(luas_lahan), 0)')
->whereColumn('map_lahan.petani_id', 'petani.id')
->whereNull('map_lahan.deleted_at')
)->orderBy('created_at', 'desc'),
'luas_lahan_asc' => $query->orderBy(
Lahan::selectRaw('COALESCE(SUM(luas_lahan), 0)')
->whereColumn('map_lahan.petani_id', 'petani.id')
->whereNull('map_lahan.deleted_at')
)->orderBy('created_at', 'desc'),
default => $query->orderBy('created_at', 'desc'),
};
$summaryPetaniIds = (clone $query)->pluck('id');
$petani = $query->paginate($request->per_page ?? 10);
$komoditas = Komoditas::select('id', 'nama')->where('is_utama', true)->get();
$lahanQuery = Lahan::whereIn('petani_id', $summaryPetaniIds)
->whereIn('komoditas_id', $komoditas->pluck('id'));
$lahanIds = (clone $lahanQuery)->pluck('id');
$totalLuasLahan = (clone $lahanQuery)->sum('luas_lahan');
$totalTanaman = Tanaman::whereIn('lahan_id', $lahanIds)->count();
$komoditasOptions = Komoditas::select('id', 'nama')->orderBy('nama')->get();
$kabupatenOptions = KabupatenKota::select('id', 'nama')->orderBy('nama')->get();
$selectedKabupaten = $filters['kabupaten_kota_id']
? KabupatenKota::select('id', 'nama')->find($filters['kabupaten_kota_id'])
: null;
$kecamatanOptions = $filters['kabupaten_kota_id']
? Kecamatan::select('id', 'nama')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get()
: collect();
$desaOptions = $request->filled('kecamatan_id')
? DesaKelurahan::select('id', 'nama')->where('kecamatan_id', $request->kecamatan_id)->orderBy('nama')->get()
: collect();
$lahanSummaryQuery = Lahan::query()
->whereHas('petani')
->when($request->filled('komoditas_id'), function ($query) use ($request) {
$query->where('komoditas_id', $request->komoditas_id);
})
->when($filters['kabupaten_kota_id'], function ($query) use ($filters) {
$query->whereHas('desaKelurahan.kecamatan', function ($kecamatan) use ($filters) {
$kecamatan->where('kabupaten_kota_id', $filters['kabupaten_kota_id']);
});
})
->when($request->filled('kecamatan_id'), function ($query) use ($request) {
$query->whereHas('desaKelurahan', function ($desa) use ($request) {
$desa->where('kecamatan_id', $request->kecamatan_id);
});
})
->when($request->filled('desa_kelurahan_id'), function ($query) use ($request) {
$query->where('desa_kelurahan_id', $request->desa_kelurahan_id);
})
->when($request->filled('search'), function ($query) use ($search) {
$query->whereHas('petani', function ($petani) use ($search) {
$petani->whereRaw('lower(nama) like ?', ["%{$search}%"])
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
});
})
->when($userRole === 'fasilitator', function ($query) {
$query->whereHas('petani', function ($petani) {
$petani->where('created_by', auth()->id());
});
});
$totalLahan = (clone $lahanSummaryQuery)->count();
$totalLuasLahan = (clone $lahanSummaryQuery)->sum('luas_lahan');
$totalTanaman = Tanaman::whereIn('lahan_id', (clone $lahanSummaryQuery)->select('id'))
->when($request->filled('komoditas_id'), function ($query) use ($request) {
$query->where('komoditas_id', $request->komoditas_id);
})
->count();
$case = "
CASE
@@ -81,7 +193,25 @@ class PetaniController extends Controller
->groupByRaw($case)
->get();
return view('produktivitas.petani.index', compact('petani', 'provinsi', 'komoditas', 'skor', 'totalLuasLahan', 'totalTanaman'));
return view('produktivitas.petani.index', compact(
'petani',
'provinsi',
'komoditas',
'komoditasOptions',
'kabupatenOptions',
'selectedKabupaten',
'kecamatanOptions',
'desaOptions',
'filters',
'skor',
'totalLahan',
'totalLuasLahan',
'totalTanaman',
'totalPetani',
'totalPetaniLakiLaki',
'totalPetaniPerempuan',
'showFasilitatorColumn'
));
}
public function create()
@@ -49,20 +49,43 @@ class ProfilPetaniController extends Controller
$skorPetani = SkorPetani::where('petani_id', $id)->first();
$enumerator = Profile::where('user_id', $petani->created_by)->first();
$komoditasUtama = Komoditas::where('id', $petani->komoditas_id)->first();
$lahan = Lahan::where('petani_id', $id )->get();
$lahan = Lahan::query()
->select(['id', 'petani_id', 'nama', 'path', 'luas_lahan', 'komoditas_id', 'desa_kelurahan_id'])
->with([
'komoditas:id,nama',
'desaKelurahan:id,nama,path,kecamatan_id',
'desaKelurahan.kecamatan:id,nama,path',
])
->where('petani_id', $id)
->get();
$desaKelurahan = DesaKelurahan::with('kecamatan')
->where('id', $petani->desa_kelurahan_id)
->first();
$ktpMasked = $petani->ktp ? Str::mask($petani->ktp, '*', 4, -4) : '-';
$profiling = $skorPetani ? $this->buildProfiling($skorPetani) : null;
$assessmentFields = [
'skor_kesiapan_adopsi',
'skor_d2_sistem_praktik_budidaya',
'skor_d4_sosial_ekonomi',
'skor_d6_rantai_nilai_akses_pasar',
'skor_komp_1_d4_k3_motivasi_kapasitas_adopsi',
'skor_komp_2_d4_k2_ekonomi_rumah_tangga',
'skor_komp_3_d2_praktik_budidaya_gap',
'skor_komp_4_d4_k4_keterlibatan_kelembagaan',
'skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan',
'skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar',
];
$hasAssessment = $skorPetani !== null && collect($assessmentFields)->every(
fn (string $field) => $skorPetani->{$field} !== null
);
$profiling = $hasAssessment ? $this->buildProfiling($skorPetani) : null;
$tanaman = Tanaman::where('petani_id', $petani->id)
->where('komoditas_id', $petani->komoditas_id)
->first();
if ($tanaman) {
if ($tanaman?->updated_at && $tanaman->umur_tanaman !== null) {
$tahunInputTanaman = $tanaman->updated_at->year;
$umurTanaman = (int) $tanaman->umur_tanaman;
$tahunSekarang = now()->year;
@@ -162,8 +185,8 @@ class ProfilPetaniController extends Controller
$d4SosialEkonomi = null;
$terdaftarKelompok = $sosialEkonomi->terdaftar_kelompok_tani;
$peranKelompok = $sosialEkonomi->peran_dalam_kelompok;
$terdaftarKelompok = $sosialEkonomi?->terdaftar_kelompok_tani;
$peranKelompok = $sosialEkonomi?->peran_dalam_kelompok;
if ($sosialEkonomi) {
@@ -207,8 +230,8 @@ class ProfilPetaniController extends Controller
'icon' => 'tabler-minus',
],
default => [
'label' => Str::headline($status),
'short' => Str::headline($status),
'label' => filled($status) ? Str::headline($status) : 'Belum diisi',
'short' => filled($status) ? Str::headline($status) : 'Belum diisi',
'color' => 'secondary',
'icon' => 'tabler-help-circle',
],
@@ -434,12 +457,61 @@ class ProfilPetaniController extends Controller
],
];
// Report missing profile data without treating an unanswered field as zero.
$missingSections = [];
$identityFields = [
'nama' => 'Nama', 'ktp' => 'NIK', 'tanggal_lahir' => 'Tanggal lahir',
'jenis_kelamin' => 'Jenis kelamin', 'pendidikan_terakhir' => 'Pendidikan terakhir',
'status_perkawinan' => 'Status perkawinan',
];
foreach ($identityFields as $field => $label) {
if (blank($petani->{$field})) {
$missingSections[] = $label;
}
}
if (!$desaKelurahan?->nama || !$desaKelurahan?->kecamatan?->nama) {
$missingSections[] = 'Wilayah desa/kecamatan';
}
if (!$komoditasUtama?->nama) {
$missingSections[] = 'Komoditas utama';
}
if ($lahan->isEmpty()) {
$missingSections[] = 'Data lahan';
}
if ($tahunTanam === null) {
$missingSections[] = 'Umur tanaman';
}
if (!$praktikBudidaya || $indikatorOnFarm->contains(fn ($item) => $item['skor'] === null)) {
$missingSections[] = 'Praktik budidaya';
}
if ($surveyPanen?->hasil_komoditas_utama_permusim === null) {
$missingSections[] = 'Survei panen';
}
$socialFields = [
'jumlah_tanggungan', 'skor_kecukupan_penghasilan', 'skor_kemampuan_menabung',
'status_kepemilikan_lahan', 'terdaftar_kelompok_tani', 'status_rekening_tabungan',
'pengalaman_kredit_kebun', 'keterikatan_hutang_panen', 'praktik_ijon',
'kebebasan_memilih_pembeli',
];
if (!$sosialEkonomi || collect($socialFields)->contains(fn ($field) => blank($sosialEkonomi->{$field}))) {
$missingSections[] = 'Sosial ekonomi';
}
if (!$aksesPasar || blank($aksesPasar->saluran_terdahulu)) {
$missingSections[] = 'Akses pasar';
}
if (!$hasAssessment) {
$missingSections[] = 'Penilaian kesiapan adopsi belum lengkap';
}
$data = [
'missingSections' => $missingSections,
'hasAssessment' => $hasAssessment,
'petani' => $petani,
'skorPetani' => $skorPetani,
'enumerator' => $enumerator,
'komoditasUtama' => $komoditasUtama,
'lahan' => $lahan,
'farmerLandMap' => $this->farmerLandMapData($lahan),
'desaKelurahan' => $desaKelurahan,
'ktpMasked' => $ktpMasked,
'profiling' => $profiling,
@@ -447,7 +519,6 @@ class ProfilPetaniController extends Controller
'umurTanaman' => $umurTanaman,
'surveyPanen' => $surveyPanen,
'praktikBudidaya' => $praktikBudidaya,
'praktikBudidaya' => $praktikBudidaya,
'indikatorOnFarm' => $indikatorOnFarm,
'detailOnFarm' => $detailOnFarm,
'sosialEkonomi' => $sosialEkonomi,
@@ -464,6 +535,35 @@ class ProfilPetaniController extends Controller
return view('produktivitas.profil-petani.index', $data);
}
private function farmerLandMapData(\Illuminate\Support\Collection $lands): array
{
$regions = ['desa' => [], 'kecamatan' => []];
$plots = [];
foreach ($lands as $land) {
$plot = [
'id' => $land->id,
'nama' => $land->nama,
'path' => $land->path,
'luas' => $land->luas_lahan,
'komoditas' => $land->komoditas?->nama,
];
foreach (['desa' => $land->desaKelurahan, 'kecamatan' => $land->desaKelurahan?->kecamatan] as $level => $region) {
$plot[$level . '_id'] = $region?->id;
if ($region && !isset($regions[$level][$region->id])) {
$regions[$level][$region->id] = [
'id' => $region->id,
'nama' => $region->nama,
'path' => $region->path,
];
}
}
$plots[] = $plot;
}
return ['lands' => $plots, 'regions' => $regions];
}
private function getScoreColor(?int $score): string
{
if ($score === null) {
+10
View File
@@ -22,10 +22,18 @@ class AksesPasar extends Model
protected $fillable = [
'uid',
'petani_id',
'tujuan_penjualan',
'tujuan_penjualan_lainnya',
'sifat_hubungan_bisnis',
'bentuk_penjualan',
'frekuensi_penjualan',
'skor_pengalaman',
'saluran_terdahulu',
'saluran_terdahulu_lainnya',
'alasan_berhenti',
'tahu_harga_sebelumnya',
'sumber_info_harga',
'sumber_info_harga_lainnya',
'praktik_fermentasi',
'proses_fermentasi',
'model_fermentasi_memungkinkan',
@@ -44,7 +52,9 @@ class AksesPasar extends Model
];
protected $casts = [
'tujuan_penjualan' => 'array',
'saluran_terdahulu' => 'array',
'sumber_info_harga' => 'array',
];
protected static function booted(): void
@@ -61,7 +61,7 @@ return new class extends Migration
$table->integer('harga_tahun_lalu')->nullable();
$table->integer('harga_dua_tahun_lalu')->nullable();
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
$table ->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
$table ->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->text('tujuan_penjualan')->nullable();
$table->string('tujuan_penjualan_lainnya')->nullable();
$table->text('sifat_hubungan_bisnis')->nullable();
$table->text('bentuk_penjualan')->nullable();
$table->string('frekuensi_penjualan')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->dropColumn([
'tujuan_penjualan',
'tujuan_penjualan_lainnya',
'sifat_hubungan_bisnis',
'bentuk_penjualan',
'frekuensi_penjualan'
]);
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->string('tahu_harga_sebelumnya')->nullable();
$table->text('sumber_info_harga')->nullable();
$table->string('sumber_info_harga_lainnya')->nullable();
});
}
public function down(): void
{
Schema::table('akses_pasar', function (Blueprint $table) {
$table->dropColumn([
'tahu_harga_sebelumnya',
'sumber_info_harga',
'sumber_info_harga_lainnya',
]);
});
}
};
+82
View File
@@ -103,3 +103,85 @@
text-align: center;
margin-top: 1rem;
}
/* Sidebar icon tiles: keep the icon mask on the inner element. */
#layout-menu .menu-inner > .menu-item > .menu-link > .sidebar-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 30px;
width: 30px;
height: 30px;
border-radius: 9px;
color: var(--sidebar-icon-color, var(--bs-secondary));
background: var(--sidebar-icon-bg, rgba(var(--bs-secondary-rgb), 0.1));
transition: background-color 180ms ease, color 180ms ease, transform 180ms ease;
}
#layout-menu .sidebar-icon > .ti {
width: 21px;
height: 21px;
font-size: 21px;
}
#layout-menu .sidebar-icon:has(.tabler-dashboard),
#layout-menu .sidebar-icon:has(.tabler-database) {
--sidebar-icon-color: var(--bs-primary);
--sidebar-icon-bg: rgba(var(--bs-primary-rgb), 0.12);
}
#layout-menu .sidebar-icon:has(.tabler-user-circle),
#layout-menu .sidebar-icon:has(.tabler-id-badge-2),
#layout-menu .sidebar-icon:has(.tabler-plant) {
--sidebar-icon-color: var(--bs-success);
--sidebar-icon-bg: rgba(var(--bs-success-rgb), 0.12);
}
#layout-menu .sidebar-icon:has(.tabler-users-group),
#layout-menu .sidebar-icon:has(.tabler-sitemap) {
--sidebar-icon-color: var(--bs-info);
--sidebar-icon-bg: rgba(var(--bs-info-rgb), 0.12);
}
#layout-menu .sidebar-icon:has(.tabler-building-community),
#layout-menu .sidebar-icon:has(.tabler-heart-handshake) {
--sidebar-icon-color: var(--bs-warning);
--sidebar-icon-bg: rgba(var(--bs-warning-rgb), 0.12);
}
#layout-menu .sidebar-icon:has(.tabler-map-2),
#layout-menu .sidebar-icon:has(.tabler-user-shield),
#layout-menu .sidebar-icon:has(.tabler-route) {
--sidebar-icon-color: var(--bs-primary);
--sidebar-icon-bg: rgba(var(--bs-primary-rgb), 0.12);
}
#layout-menu .menu-item > .menu-link:hover > .sidebar-icon,
#layout-menu .menu-item > .menu-link:focus-visible > .sidebar-icon {
transform: translateY(-1px);
}
#layout-menu .menu-inner > .menu-item.active > .menu-link > .sidebar-icon {
color: inherit;
background: rgba(255, 255, 255, 0.18);
}
@media (prefers-reduced-motion: reduce) {
#layout-menu .menu-inner > .menu-item > .menu-link > .sidebar-icon {
transition: none;
transform: none;
}
}
/* Submenu icons replace the template's decorative bullets. */
#layout-menu .menu-sub > .menu-item > .menu-link::before { display: none; }
#layout-menu .menu-sub .sidebar-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 22px;
width: 22px;
height: 22px;
color: inherit;
}
#layout-menu .menu-sub .sidebar-icon > .ti { width: 19px; height: 19px; font-size: 19px; }
+63
View File
@@ -37,10 +37,73 @@ window.config = {
danger: window.Helpers.getCssVar('danger-bg-subtle'),
dark: window.Helpers.getCssVar('dark-bg-subtle')
},
chartColors: {
series: ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'],
semantic: {
primary: '#9D8EF0',
secondary: '#9EA6B0',
success: '#6FD68F',
info: '#5FC7D6',
warning: '#FFB46F',
danger: '#F47C86',
purple: '#9D8EF0',
teal: '#65D8BD',
pink: '#F47C86',
gray: '#9EA6B0'
},
text: {
primary: '#7367F0',
secondary: '#808390',
success: '#28C76F',
info: '#00BAD1',
warning: '#FF9F43',
danger: '#FF4C51',
purple: '#7367F0',
teal: '#20C997',
pink: '#FF4C51',
gray: '#808390'
},
categories: {
coffee: '#6FD68F',
vanilla: '#FFB46F',
cacao: '#9D8EF0',
candlenut: '#9EA6B0',
shade: '#65D8BD',
policyPurple: '#9D8EF0',
policyPink: '#F47C86',
marketGreen: '#6FD68F',
marketRed: '#F47C86',
marketBlue: '#5FC7D6'
},
grid: '#ECF0F6'
},
fontFamily: window.Helpers.getCssVar('font-family-base'),
enableMenuLocalStorage: true // Enable menu state with local storage support
};
// Choose the label color with the higher contrast against each chart slice.
window.config.chartTextColor = function (color) {
let hex = String(color || '').trim().replace(/^#/, '');
if (/^[0-9a-f]{3}$/i.test(hex)) {
hex = hex.split('').map(value => value + value).join('');
}
if (!/^[0-9a-f]{6}$/i.test(hex)) return '#212529';
const luminance = function (value) {
const channels = value.match(/../g).map(channel => {
const srgb = parseInt(channel, 16) / 255;
return srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
});
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
};
const background = luminance(hex);
const dark = luminance('212529');
const darkContrast = (Math.max(background, dark) + 0.05) / (Math.min(background, dark) + 0.05);
const lightContrast = 1.05 / (background + 0.05);
return darkContrast >= lightContrast ? '#212529' : '#ffffff';
};
window.assetsPath = document.documentElement.getAttribute('data-assets-path');
window.templateName = document.documentElement.getAttribute('data-template');
+24 -29
View File
@@ -9,6 +9,9 @@
cardColor = config.colors.cardColor;
labelColor = config.colors.textMuted;
headingColor = config.colors.headingColor;
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
// swiper loop and autoplay
// --------------------------------------------------------------------
@@ -48,16 +51,10 @@
grid: {
show: false
},
colors: [config.colors.success],
colors: [pastelSemantic.success || '#6FD68F'],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.4,
gradientToColors: [config.colors.cardColor],
opacityTo: 0.1,
stops: [0, 100]
}
type: 'solid',
opacity: 0.22
},
dataLabels: {
enabled: false
@@ -151,13 +148,13 @@
}
},
colors: [
config.colors_label.primary,
config.colors_label.primary,
config.colors_label.primary,
config.colors_label.primary,
config.colors.primary,
config.colors_label.primary,
config.colors_label.primary
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.primary || '#9D8EF0',
pastelSemantic.secondary || '#9EA6B0',
pastelSemantic.secondary || '#9EA6B0'
],
dataLabels: {
enabled: false
@@ -262,18 +259,10 @@
}
}
},
colors: [config.colors.primary],
colors: [pastelSemantic.primary || '#9D8EF0'],
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
shadeIntensity: 0.5,
gradientToColors: [config.colors.primary],
inverseColors: true,
opacityFrom: 1,
opacityTo: 0.6,
stops: [30, 70, 100]
}
type: 'solid',
opacity: 0.9
},
stroke: {
dashArray: 10
@@ -356,7 +345,10 @@
}
},
colors: [config.colors.primary, config.colors.secondary],
colors: [
pastelSeries[0] || '#9D8EF0',
pastelSeries[1] || '#6FD68F'
],
dataLabels: {
enabled: false
},
@@ -369,7 +361,10 @@
legend: {
show: false
},
colors: [config.colors.primary, config.colors.secondary],
colors: [
pastelSeries[0] || '#9D8EF0',
pastelSeries[1] || '#6FD68F'
],
fill: {
opacity: 1
},
+13 -9
View File
@@ -238,6 +238,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -255,7 +259,7 @@
{
label: 'Desa Mengadopsi (Realisasi)',
data: [0, 3, 8, 15], // Kumulatif: 3 Desa Inti + 12 Baru
backgroundColor: '#8b5cf6', // Purple-500
backgroundColor: pastelCategory.policyPurple || '#9D8EF0',
borderRadius: 4,
order: 2
},
@@ -263,12 +267,12 @@
label: 'Target Perluasan ToC',
type: 'line',
data: [3, 5, 10, 15],
borderColor: '#d946ef', // Fuchsia-500
borderColor: pastelCategory.policyPink || '#F47C86',
backgroundColor: 'transparent',
borderWidth: 3,
borderDash: [5, 5],
tension: 0.3,
pointBackgroundColor: '#d946ef',
pointBackgroundColor: pastelCategory.policyPink || '#F47C86',
order: 1
}
]
@@ -280,7 +284,7 @@
scales: {
y: {
beginAtZero: true,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: { stepSize: 5 }
},
x: { grid: { display: false } }
@@ -309,10 +313,10 @@
datasets: [{
data: [4, 2, 1, 1],
backgroundColor: [
'#f59e0b', // Amber-500
'#10b981', // Emerald-500
'#3b82f6', // Blue-500
'#8b5cf6' // Purple-500
pastelSemantic.warning || '#FFB46F',
pastelSemantic.success || '#6FD68F',
pastelSemantic.info || '#5FC7D6',
pastelCategory.policyPurple || '#9D8EF0'
],
borderWidth: 2,
borderColor: '#ffffff',
@@ -335,4 +339,4 @@
});
</script>
@endpush
@endpush
+27 -27
View File
@@ -7,9 +7,9 @@
transition: transform .2s ease;
}
.tk-stat:hover { transform: translateX(4px); }
.tk-stat.green { border-left-color: #28a745; }
.tk-stat.blue { border-left-color: #007bff; }
.tk-stat.amber { border-left-color: #fd7e14; }
.tk-stat.green { border-left-color: #6FD68F; }
.tk-stat.blue { border-left-color: #5FC7D6; }
.tk-stat.amber { border-left-color: #FFB46F; }
.tk-stat-lbl {
font-size: 10px; text-transform: uppercase;
@@ -88,38 +88,38 @@
'new_month' => 12,
'enumerators' => [
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#28a745','bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#007bff','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#fd7e14','bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#dc3545','bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#6FD68F','bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#5FC7D6','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#FFB46F','bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#F47C86','bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
],
'recent' => [
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
],
'komoditas' => [
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#28a745','arc'=>174],
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#007bff','arc'=>96],
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#fd7e14','arc'=>38],
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#6f42c1','arc'=>26],
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#6FD68F','arc'=>174],
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#9D8EF0','arc'=>96],
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#FFB46F','arc'=>38],
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#9EA6B0','arc'=>26],
],
'kecamatan' => [
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#28a745'],
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#007bff'],
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#fd7e14'],
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#6FD68F'],
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#5FC7D6'],
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#FFB46F'],
],
'pending' => [
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
],
'pending_more' => 32,
];
@@ -258,10 +258,10 @@
@foreach($d['recent'] as $s)
@php
$bc = match(true) {
$s['pct'] === 100 => '#28a745',
$s['pct'] >= 50 => '#007bff',
$s['pct'] >= 20 => '#fd7e14',
default => '#dc3545',
$s['pct'] === 100 => '#6FD68F',
$s['pct'] >= 50 => '#5FC7D6',
$s['pct'] >= 20 => '#FFB46F',
default => '#F47C86',
};
@endphp
<tr>
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -208,6 +208,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -225,7 +229,7 @@
{
label: 'Mentah/Gelondong (Rp)',
data: [35000, 200000, 25000, 15000],
backgroundColor: '#9ca3af', // Gray
backgroundColor: pastelSemantic.gray || '#9EA6B0',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
barPercentage: 0.6,
categoryPercentage: 0.8
@@ -233,7 +237,7 @@
{
label: 'Produk Olahan (Rp)',
data: [120000, 1500000, 80000, 45000],
backgroundColor: '#d97706', // Amber-600
backgroundColor: pastelSemantic.warning || '#FFB46F',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
barPercentage: 0.6,
categoryPercentage: 0.8
@@ -246,7 +250,7 @@
scales: {
y: {
beginAtZero: true,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: {
callback: function(value) {
if (value >= 1000000) return 'Rp ' + (value/1000000) + ' Jt';
@@ -282,8 +286,8 @@
{
label: 'Kopi Bubuk/Roasted',
data: [12, 15, 18, 25, 30, 45],
borderColor: '#6F4E37',
backgroundColor: 'rgba(111, 78, 55, 0.1)',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: 'rgba(104, 193, 124, 0.16)',
fill: true,
tension: 0.4,
borderWidth: 2
@@ -291,7 +295,7 @@
{
label: 'Kakao Fermentasi',
data: [5, 8, 12, 15, 22, 28],
borderColor: '#D2691E',
borderColor: pastelCategory.cacao || '#9D8EF0',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2
@@ -303,9 +307,9 @@
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: {
title: { display: true, text: 'Volume (Ton)' },
grid: { color: '#f3f4f6' }
y: {
title: { display: true, text: 'Volume (Ton)' },
grid: { color: gridColor }
},
x: { grid: { display: false } }
},
@@ -318,4 +322,4 @@
});
</script>
@endpush
@endpush
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -212,6 +212,9 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -230,8 +233,8 @@
label: 'Total Produksi Lahan (Ton)',
type: 'line',
data: [100, 110, 125, 145],
borderColor: '#10b981', // Emerald-500
backgroundColor: '#10b981',
borderColor: pastelCategory.marketGreen || '#6FD68F',
backgroundColor: pastelCategory.marketGreen || '#6FD68F',
borderWidth: 3,
tension: 0.3,
fill: false
@@ -239,14 +242,14 @@
{
label: 'Tengkulak / Pasar Bebas',
data: [80, 65, 40, 25],
backgroundColor: '#ef4444', // Red-500
backgroundColor: pastelCategory.marketRed || '#F47C86',
stack: 'Stack 0',
borderRadius: { topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 4 }
},
{
label: 'Kelembagaan (Koperasi/BUMDes)',
data: [20, 45, 85, 120],
backgroundColor: '#3b82f6', // Blue-500
backgroundColor: pastelCategory.marketBlue || '#5FC7D6',
stack: 'Stack 0',
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 }
}
@@ -260,7 +263,7 @@
y: {
stacked: true,
beginAtZero: true,
grid: { color: '#f3f4f6' }
grid: { color: gridColor }
},
x: {
stacked: true,
@@ -292,20 +295,20 @@
{
label: 'Kopi & Kakao',
data: [150, 280, 320, 500, 650, 850],
borderColor: '#6F4E37',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2,
pointBackgroundColor: '#6F4E37'
pointBackgroundColor: pastelCategory.coffee || '#6FD68F'
},
{
label: 'Vanili & Kemiri',
data: [50, 120, 210, 350, 420, 600],
borderColor: '#eab308',
borderColor: pastelCategory.vanilla || '#FFB46F',
backgroundColor: 'transparent',
tension: 0.4,
borderWidth: 2,
pointBackgroundColor: '#eab308'
pointBackgroundColor: pastelCategory.vanilla || '#FFB46F'
}
]
},
@@ -316,7 +319,7 @@
scales: {
y: {
title: { display: false },
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: {
callback: function(value) {
return 'Rp ' + value + ' Jt';
@@ -341,4 +344,4 @@
});
</script>
@endpush
@endpush
+671
View File
@@ -0,0 +1,671 @@
@extends('layout.main')
@push('style')
<style>
.pasar-filter-card .filter-item {
min-width: 190px;
}
.pasar-kpi-value {
color: var(--bs-heading-color);
font-size: 1.6rem;
font-weight: 600;
line-height: 1.2;
overflow-wrap: anywhere;
}
.pasar-kpi-label {
color: var(--bs-heading-color);
font-size: 0.9375rem;
font-weight: 400;
}
.pasar-kpi-desc {
font-size: 0.8125rem;
line-height: 1.4;
}
.chart-group-header-toggle {
cursor: pointer;
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.08);
border-inline-start: 4px solid var(--bs-primary, #9D8EF0);
box-shadow: none;
transition: background-color 0.2s ease;
}
.chart-group-header-toggle:hover {
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.13);
}
.chart-group-header-toggle .card-body {
padding: 12px 16px;
}
.chart-group-header-toggle h5 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 2px;
}
.chart-group-header-toggle:focus-visible {
outline: 2px solid var(--bs-primary);
outline-offset: 3px;
}
.chart-group-icon-toggle {
width: 34px;
height: 34px;
flex-shrink: 0;
}
.chart-group-icon-toggle .icon-base {
font-size: 24px;
width: 24px;
height: 24px;
}
.chart-group-panel.show,
.chart-group-panel.collapsing {
margin-bottom: 1.5rem;
}
.pasar-chart-card .card-title h5 {
color: var(--bs-heading-color);
font-size: 1.125rem;
font-weight: 500;
line-height: 1.35;
letter-spacing: 0;
}
.pasar-chart-card .card-title small,
.chart-group-header-toggle p {
font-size: 0.8125rem;
line-height: 1.4;
}
.pasar-chart {
min-height: 330px;
}
.pasar-chart-empty {
min-height: 330px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bs-secondary-color);
font-size: 0.8125rem;
text-align: center;
}
.pasar-chart-card .apexcharts-tooltip {
background: transparent !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
white-space: normal;
max-width: 260px;
}
.pasar-chart-tooltip {
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
padding: 8px 12px;
font-size: 13px;
font-weight: 500;
line-height: 1.25;
}
.pasar-trend-tooltip-row {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 6px;
}
@media (max-width: 575.98px) {
.pasar-filter-card .filter-item {
width: 100%;
}
}
</style>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
<div class="row g-4 mb-4">
<form id="form-filter" class="col-12" method="GET" action="{{ route('dashboard.pasar') }}">
<div class="card card-border-shadow-primary pasar-filter-card">
<div class="card-body py-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-map-2 me-2"></i>
<span class="fw-medium">Wilayah</span>
</div>
<div class="filter-item">
<select id="filter-provinsi" class="form-select form-select-sm" name="provinsi_id" data-default="{{ $provinsi_id }}">
<option value="">Semua Provinsi</option>
@foreach($provinsi as $p)
<option value="{{ $p->provinsi_id }}" {{ old('provinsi_id', $provinsi_id) == $p->provinsi_id ? 'selected' : '' }}>
{{ $p->provinsi }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select id="filter-kabkota" class="form-select form-select-sm" name="kabupatenKota_id" data-default="{{ $kabupatenKota_id }}" disabled>
<option value="">Semua Kabupaten/Kota</option>
</select>
</div>
<div class="filter-item">
<select id="filter-kecamatan" class="form-select form-select-sm" name="kecamatan_id" data-default="{{ $kecamatan_id }}" disabled>
<option value="">Semua Kecamatan</option>
</select>
</div>
<div class="filter-item">
<select id="filter-desa" class="form-select form-select-sm" name="desaKelurahan_id" data-default="{{ $desaKelurahan_id }}" disabled>
<option value="">Semua Desa/Kelurahan</option>
</select>
</div>
</div>
<hr class="my-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
<span class="fw-medium">Data</span>
</div>
<div class="filter-item">
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
<option value="">Semua Komoditas</option>
@foreach ($komoditas as $item)
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
{{ $item->nama }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select id="filter-segmen" class="form-select form-select-sm" name="segmen">
<option value="">Semua Segmen</option>
@foreach ($segments as $segment)
<option value="{{ $segment['name'] }}" {{ old('segmen', $segmen) == $segment['name'] ? 'selected' : '' }}>
{{ $segment['name'] }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-filter me-1"></i>
Filter
</button>
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
<i class="icon-base ti tabler-refresh me-1"></i>
Reset
</button>
</div>
</div>
</div>
</form>
</div>
<div class="row g-4 mb-4">
@foreach ($priceCards as $priceCard)
<div class="col-12 col-md-6 col-xl-3">
<div class="card card-border-shadow-{{ $priceCard['variant'] }} h-100">
<div class="card-body">
<div class="d-flex flex-column align-items-start gap-3">
<div class="avatar avatar-lg">
<span class="avatar-initial rounded bg-label-{{ $priceCard['variant'] }}">
<i class="icon-base ti {{ $priceCard['icon'] }} icon-28px"></i>
</span>
</div>
<div>
<div class="d-flex align-items-baseline gap-2 flex-wrap">
<h4 class="mb-0 pasar-kpi-value">{{ $priceCard['value'] }}</h4>
<small class="text-body-secondary">{{ $priceCard['unit'] }}</small>
</div>
<p class="mb-0 mt-1 pasar-kpi-label">{{ $priceCard['label'] }}</p>
<small class="text-body-secondary pasar-kpi-desc">{{ $priceCard['description'] }}</small>
</div>
</div>
<div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-2 flex-wrap">
<span class="badge bg-label-{{ $priceCard['variant'] }}">{{ $priceCard['records'] > 0 ? 'Rata-rata harga' : 'Belum ada data' }}</span>
<small class="text-body-secondary">{{ number_format($priceCard['records'], 0, ',', '.') }} data harga</small>
</div>
</div>
</div>
</div>
@endforeach
</div>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#saluran-harga-charts"
aria-expanded="false" aria-controls="saluran-harga-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Saluran &amp; Harga</h5>
<p class="mb-0 text-body-secondary">Saluran penjualan, hubungan dengan pembeli, waktu penjualan, akses ke saluran alternatif, dan tren harga.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
@php
$chartDefinitions = [
'saluranPenjualan' => ['title' => 'Saluran Penjualan Utama', 'description' => 'Kepada siapa petani biasanya menjual hasil panen?', 'variant' => 'success'],
'hubunganPembeli' => ['title' => 'Sifat Hubungan dengan Pembeli', 'description' => 'Hubungan jual petani dengan pembeli utama saat ini.', 'variant' => 'primary'],
'waktuPenjualan' => ['title' => 'Waktu Penjualan Hasil Panen', 'description' => 'Kapan petani biasanya menjual hasil panen?', 'variant' => 'warning'],
'aksesSaluranLain' => ['title' => 'Pengalaman & Akses ke Saluran Lain', 'description' => 'Pengalaman menjual ke tempat lain dan akses yang masih dimiliki.', 'variant' => 'info'],
];
@endphp
<div class="collapse chart-group-panel" id="saluran-harga-charts">
<div class="row g-4 mb-4">
@foreach ($chartDefinitions as $key => $definition)
<div class="col-12 col-lg-6">
<div class="card pasar-chart-card h-100">
<div class="card-header d-flex align-items-start justify-content-between gap-2">
<div class="card-title mb-0">
<h5 class="m-0 me-2" id="title-{{ $key }}">{{ $definition['title'] }}</h5>
<small class="text-body-secondary">{{ $definition['description'] }}</small>
</div>
</div>
<div class="card-body d-flex flex-column">
<div id="chart-{{ $key }}" class="pasar-chart" role="img" aria-labelledby="title-{{ $key }}" aria-describedby="summary-{{ $key }}"></div>
<span id="summary-{{ $key }}" class="visually-hidden">
@foreach ($marketCharts[$key]['labels'] as $index => $label)
{{ $label }}: {{ $marketCharts[$key]['series'][$index] }} responden ({{ $marketCharts[$key]['percentages'][$index] }}%).
@endforeach
</span>
</div>
</div>
</div>
@endforeach
@forelse ($priceTrend['commodities'] as $commodity)
<div class="col-12 col-lg-6">
<div class="card pasar-chart-card h-100">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2" id="title-tren-harga-{{ $commodity['id'] }}">Tren Harga 3 Musim — {{ $commodity['name'] }}</h5>
<small class="text-body-secondary">Rata-rata harga jual per bentuk produk selama tiga periode panen (Rp/kg).</small>
</div>
</div>
<div class="card-body">
<div id="chart-tren-harga-{{ $commodity['id'] }}" class="pasar-chart" role="img" aria-labelledby="title-tren-harga-{{ $commodity['id'] }}" aria-describedby="summary-tren-harga-{{ $commodity['id'] }}"></div>
<span id="summary-tren-harga-{{ $commodity['id'] }}" class="visually-hidden"></span>
</div>
</div>
</div>
@empty
<div class="col-12">
<div class="card pasar-chart-card">
<div class="card-header">
<div class="card-title mb-0"><h5 class="m-0">Tren Harga 3 Musim</h5></div>
</div>
<div class="card-body pasar-chart-empty">Belum ada komoditas untuk filter yang dipilih.</div>
</div>
</div>
@endforelse
</div>
</div>
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
data-bs-toggle="collapse" data-bs-target="#kesiapan-premium-charts"
aria-expanded="false" aria-controls="kesiapan-premium-charts" aria-label="Tampilkan chart">
<div class="card-body d-flex justify-content-between align-items-center gap-3">
<div>
<h5>Kesiapan Pasar Premium</h5>
<p class="mb-0 text-body-secondary">Kesadaran petani tentang pasar premium, persyaratan masuk, dan pengalaman mengaksesnya.</p>
</div>
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
<i class="icon-base ti tabler-chevron-down"></i>
</span>
</div>
</div>
<div class="collapse chart-group-panel" id="kesiapan-premium-charts">
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card pasar-chart-card">
<div class="card-header">
<div class="card-title mb-0">
<h5 class="m-0 me-2" id="title-kesadaranPremium">Kesadaran Pasar Premium</h5>
<small class="text-body-secondary">Pengetahuan tentang pasar premium dan persyaratan yang dibutuhkan untuk masuk ke pasar tersebut.</small>
</div>
</div>
<div class="card-body">
<div id="chart-kesadaranPremium" class="pasar-chart" role="img" aria-labelledby="title-kesadaranPremium" aria-describedby="summary-kesadaranPremium"></div>
<span id="summary-kesadaranPremium" class="visually-hidden">
@foreach ($premiumAwareness['fullLabels'] as $index => $label)
{{ $label }}: {{ $premiumAwareness['series'][$index] }} responden ({{ $premiumAwareness['percentages'][$index] }}%).
@endforeach
</span>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const wilayah = @json($wilayah);
const marketCharts = @json($marketCharts);
const chartDefinitions = @json($chartDefinitions);
const priceTrend = @json($priceTrend);
const premiumAwareness = @json($premiumAwareness);
function escapeHtml(value) {
const element = document.createElement('span');
element.textContent = String(value);
return element.innerHTML;
}
function formatNumber(value, digits = 0) {
return Number(value).toLocaleString('id-ID', { maximumFractionDigits: digits });
}
const compactLabels = {
saluranPenjualan: ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Pembeli langsung/eksportir', 'Lainnya'],
waktuPenjualan: ['Langsung setelah panen', 'Ditumpuk beberapa waktu', 'Saat butuh uang mendesak'],
aksesSaluranLain: ['Tidak pernah', 'Berhenti, tanpa akses', 'Berhenti, bisa kembali', 'Sesekali masih menjual', 'Akses alternatif aktif'],
kesadaranPremium: ['Belum pernah mendengar', 'Belum paham persyaratan', 'Sedikit paham persyaratan', 'Paham, belum mengakses', 'Akses premium konsisten'],
};
const mobileLabels = {
saluranPenjualan: ['Tengkulak/lokal', 'Koperasi', 'BUMDes', 'Pembeli/eksportir', 'Lainnya'],
hubunganPembeli: ['Bebas pindah', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak'],
waktuPenjualan: ['Setelah panen', 'Ditumpuk dulu', 'Uang mendesak'],
aksesSaluranLain: ['Tidak pernah', 'Tanpa akses', 'Bisa kembali', 'Sesekali jual', 'Akses aktif'],
kesadaranPremium: ['Belum mendengar', 'Belum paham syarat', 'Sedikit paham', 'Paham, belum akses', 'Akses konsisten'],
};
function hasTrendData(commodity) {
return commodity && commodity.series.some(function (series) {
return series.data.some(function (value) { return value !== null && Number(value) > 0; });
});
}
function formatPrice(value) {
return value === null || value === undefined ? 'Belum ada data' : 'Rp ' + formatNumber(value) + '/kg';
}
function renderPriceTrend(commodity) {
const element = document.getElementById('chart-tren-harga-' + commodity.id);
const summary = document.getElementById('summary-tren-harga-' + commodity.id);
if (!hasTrendData(commodity) || typeof ApexCharts === 'undefined') {
element.classList.add('pasar-chart-empty');
element.textContent = !hasTrendData(commodity)
? 'Belum ada data harga untuk komoditas dan filter yang dipilih.'
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
return;
}
summary.textContent = commodity.name + '. ' + commodity.series.map(function (series) {
return series.name + ': ' + series.data.map(function (value, index) {
return priceTrend.periods[index] + ' ' + formatPrice(value);
}).join(', ');
}).join('. ');
const colors = config.chartColors.series.slice(0, 4);
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
new ApexCharts(element, {
chart: { type: 'line', height: 360, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false }, zoom: { enabled: false } },
series: commodity.series,
colors: colors,
stroke: { width: 3, curve: 'straight' },
markers: { size: 5, strokeWidth: 2, hover: { sizeOffset: 2 } },
dataLabels: { enabled: false },
xaxis: {
categories: priceTrend.periods,
tickPlacement: 'between',
labels: { rotate: 0, style: labelStyle },
axisBorder: { color: config.chartColors.grid },
axisTicks: { color: config.chartColors.grid },
},
yaxis: {
min: 0,
title: { text: 'Harga (Rp/kg)', style: { color: config.colors.bodyColor, fontFamily: config.fontFamily, fontWeight: 400 } },
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
},
grid: { borderColor: config.chartColors.grid, strokeDashArray: 4, padding: { left: 8, right: 20 } },
legend: { position: 'bottom', fontSize: '13px', fontFamily: config.fontFamily, labels: { colors: config.colors.bodyColor }, itemMargin: { horizontal: 12, vertical: 6 } },
tooltip: {
shared: true,
intersect: false,
custom: function ({ dataPointIndex }) {
const rows = commodity.series.map(function (series, index) {
return `<div class="pasar-trend-tooltip-row"><span style="color:${colors[index]}">${escapeHtml(series.name)}</span><strong>${formatPrice(series.data[dataPointIndex])}</strong></div>`;
}).join('');
return `<div class="pasar-chart-tooltip" style="background:${config.colors.cardColor};color:${config.colors.headingColor}"><strong>${escapeHtml(commodity.name)} — ${escapeHtml(priceTrend.periods[dataPointIndex])}</strong>${rows}</div>`;
},
},
responsive: [{
breakpoint: 576,
options: {
chart: { height: 420 },
legend: { fontSize: '12px', itemMargin: { horizontal: 6, vertical: 4 } },
xaxis: {
labels: {
hideOverlappingLabels: false,
style: { fontSize: '11px' },
formatter: function (value) {
return { 'Dua Tahun Lalu': '2 thn lalu', 'Tahun Lalu': 'Tahun lalu', 'Panen Terakhir': 'Terakhir' }[value] || value;
},
},
},
yaxis: {
title: { text: undefined },
labels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
},
},
}],
}).render();
}
function renderResponseChart(key, data, definition) {
const element = document.getElementById('chart-' + key);
if (!data.respondents || typeof ApexCharts === 'undefined') {
element.classList.add('pasar-chart-empty');
element.textContent = !data.respondents
? 'Belum ada data jawaban untuk filter yang dipilih.'
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
return;
}
const color = config.chartColors.semantic[definition.variant];
const borderColor = config.chartColors.grid;
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
const maxCount = Math.max(...data.series);
const tickCount = Math.min(4, maxCount);
const axisMax = Math.ceil(maxCount * 1.25 / tickCount) * tickCount;
new ApexCharts(element, {
chart: { type: 'bar', height: 330, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false } },
series: [{ name: 'Responden', data: data.series }],
colors: [color],
plotOptions: { bar: { horizontal: true, borderRadius: 5, barHeight: '62%', dataLabels: { position: 'top' } } },
dataLabels: {
enabled: true,
textAnchor: 'start',
offsetX: 6,
style: { fontSize: '12px', fontFamily: config.fontFamily, fontWeight: 500, colors: [config.colors.headingColor] },
formatter: function (value, options) {
return `${formatNumber(value)} (${formatNumber(data.percentages[options.dataPointIndex], 1)}%)`;
},
},
xaxis: {
categories: compactLabels[key] || data.labels,
min: 0,
max: axisMax,
tickAmount: tickCount,
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
axisBorder: { color: borderColor },
axisTicks: { color: borderColor },
},
yaxis: { labels: { maxWidth: 185, style: labelStyle } },
grid: { borderColor: borderColor, strokeDashArray: 4, padding: { right: 20 } },
legend: { show: false },
tooltip: {
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light',
custom: function ({ dataPointIndex }) {
return `<div class="pasar-chart-tooltip" style="background:${color};color:${config.colors.headingColor};font-family:${config.fontFamily}">${escapeHtml(data.fullLabels[dataPointIndex])}: <strong>${formatNumber(data.series[dataPointIndex])} responden (${formatNumber(data.percentages[dataPointIndex], 1)}%)</strong></div>`;
},
},
responsive: [{
breakpoint: 576,
options: {
xaxis: { categories: mobileLabels[key], tickAmount: 2 },
yaxis: { labels: { maxWidth: 125, style: { fontSize: '11px' } } },
grid: { padding: { right: 10 } },
dataLabels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
},
}],
}).render();
}
function renderMarketCharts() {
Object.entries(marketCharts).forEach(function ([key, data]) {
renderResponseChart(key, data, chartDefinitions[key]);
});
priceTrend.commodities.forEach(renderPriceTrend);
}
function setupChartPanel(panelId, renderCharts) {
const chartPanel = document.getElementById(panelId);
const chartControl = document.querySelector('.chart-group-header-toggle[aria-controls="' + chartPanel.id + '"]');
const chartIcon = chartControl.querySelector('.chart-group-icon-toggle i');
let rendered = false;
function syncChartPanelState() {
const expanded = chartPanel.classList.contains('show');
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
chartControl.setAttribute('aria-expanded', String(expanded));
chartControl.setAttribute('aria-label', label);
chartControl.title = label;
chartIcon.classList.toggle('tabler-chevron-up', expanded);
chartIcon.classList.toggle('tabler-chevron-down', !expanded);
}
chartControl.addEventListener('keydown', function (event) {
if (event.target !== chartControl || !['Enter', ' '].includes(event.key)) return;
event.preventDefault();
chartControl.click();
});
chartPanel.addEventListener('shown.bs.collapse', function (event) {
if (event.target !== chartPanel) return;
syncChartPanelState();
if (!rendered) {
renderCharts();
rendered = true;
}
window.dispatchEvent(new Event('resize'));
});
chartPanel.addEventListener('hidden.bs.collapse', function (event) {
if (event.target === chartPanel) syncChartPanelState();
});
syncChartPanelState();
}
setupChartPanel('saluran-harga-charts', renderMarketCharts);
setupChartPanel('kesiapan-premium-charts', function () {
renderResponseChart('kesadaranPremium', premiumAwareness, { variant: 'primary' });
});
function fillSelect($select, items, placeholder) {
$select.empty().append(new Option(placeholder, ''));
items.forEach(function (item) {
$select.append(new Option(item.nama, item.id));
});
}
function selectedProvinsi() {
const id = $('#filter-provinsi').val();
return wilayah.find(function (item) {
return String(item.id) === String(id);
});
}
function selectedKabupaten() {
const provinsi = selectedProvinsi();
const id = $('#filter-kabkota').val();
return (provinsi?.kabupaten || []).find(function (item) {
return String(item.id) === String(id);
});
}
function selectedKecamatan() {
const kabupaten = selectedKabupaten();
const id = $('#filter-kecamatan').val();
return (kabupaten?.kecamatan || []).find(function (item) {
return String(item.id) === String(id);
});
}
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
$('#filter-provinsi').on('change', function () {
const provinsi = selectedProvinsi();
fillSelect($('#filter-kabkota'), provinsi?.kabupaten || [], 'Semua Kabupaten/Kota');
fillSelect($('#filter-kecamatan'), [], 'Semua Kecamatan');
fillSelect($('#filter-desa'), [], 'Semua Desa/Kelurahan');
$('#filter-kabkota').prop('disabled', !provinsi).trigger('change.select2');
$('#filter-kecamatan, #filter-desa').prop('disabled', true).trigger('change.select2');
});
$('#filter-kabkota').on('change', function () {
const kabupaten = selectedKabupaten();
fillSelect($('#filter-kecamatan'), kabupaten?.kecamatan || [], 'Semua Kecamatan');
fillSelect($('#filter-desa'), [], 'Semua Desa/Kelurahan');
$('#filter-kecamatan').prop('disabled', !kabupaten).trigger('change.select2');
$('#filter-desa').prop('disabled', true).trigger('change.select2');
});
$('#filter-kecamatan').on('change', function () {
const kecamatan = selectedKecamatan();
fillSelect($('#filter-desa'), kecamatan?.desa || [], 'Semua Desa/Kelurahan');
$('#filter-desa').prop('disabled', !kecamatan).trigger('change.select2');
});
const defaultProvinsi = String($('#filter-provinsi').data('default') || '');
const defaultKabupaten = String($('#filter-kabkota').data('default') || '');
const defaultKecamatan = String($('#filter-kecamatan').data('default') || '');
const defaultDesa = String($('#filter-desa').data('default') || '');
if (defaultProvinsi) {
$('#filter-provinsi').val(defaultProvinsi).trigger('change');
if (defaultKabupaten) {
$('#filter-kabkota').val(defaultKabupaten).trigger('change');
if (defaultKecamatan) {
$('#filter-kecamatan').val(defaultKecamatan).trigger('change');
if (defaultDesa) {
$('#filter-desa').val(defaultDesa).trigger('change.select2');
}
}
}
}
$('#btn-reset-dashboard-filter').on('click', function () {
window.location.href = @json(route('dashboard.pasar'));
});
});
</script>
@endpush
+3 -2
View File
@@ -493,6 +493,7 @@ $segments = [
legendColor = config.colors.bodyColor;
borderColor = config.colors.borderColor;
headingColor = config.colors.headingColor;
const pastelSemantic = config.chartColors?.semantic || {};
var radarDimensi = new ApexCharts($("#chart-dimensi")[0],
options = {
@@ -542,7 +543,7 @@ $segments = [
highlightDataSeries: false
}
},
colors: [config.colors.primary, config.colors.info],
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
fill: {
opacity: 0.85
},
@@ -640,7 +641,7 @@ $segments = [
highlightDataSeries: false
}
},
colors: [config.colors.primary, config.colors.info],
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
fill: {
opacity: 0.85
},
File diff suppressed because it is too large Load Diff
+28 -25
View File
@@ -144,10 +144,10 @@
<div class="absolute bottom-3 left-3 bg-white/95 p-3 rounded-lg text-xs border border-gray-200 shadow-sm backdrop-blur-sm">
<div class="font-bold text-gray-700 mb-2">Legenda Tanaman:</div>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6F4E37] rounded-full shadow-sm"></div> Kopi</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#eab308] rounded-full shadow-sm"></div> Vanili</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#D2691E] rounded-full shadow-sm"></div> Kakao</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9ca3af] rounded-full shadow-sm"></div> Kemiri</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6FD68F] rounded-full shadow-sm"></div> Kopi</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#FFB46F] rounded-full shadow-sm"></div> Vanili</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9D8EF0] rounded-full shadow-sm"></div> Kakao</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9EA6B0] rounded-full shadow-sm"></div> Kemiri</div>
</div>
</div>
</div>
@@ -253,6 +253,9 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// 1. Inisialisasi Icons
if(typeof lucide !== 'undefined') {
@@ -263,19 +266,19 @@ document.addEventListener('DOMContentLoaded', function() {
const spatialData = [
{
id: 'L-001', owner: 'Yosef Neta', familySize: 5, area: '1.2 Ha', village: 'Radabata',
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(34, 197, 94, 0.15)", stroke: "#16a34a",
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(111, 214, 143, 0.24)", stroke: "#6FD68F",
plants: [
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: '#6F4E37' },
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: '#eab308' },
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: '#6F4E37' },
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: pastelCategory.vanilla || '#FFB46F' },
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
]
},
{
id: 'L-002', owner: 'Maria Goreti', familySize: 3, area: '0.8 Ha', village: 'Ubedolumolo',
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(59, 130, 246, 0.15)", stroke: "#2563eb",
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(95, 199, 214, 0.20)", stroke: "#5FC7D6",
plants: [
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: '#9ca3af' },
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: '#D2691E' },
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: pastelCategory.candlenut || '#9EA6B0' },
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: pastelCategory.cacao || '#9D8EF0' },
]
}
];
@@ -440,24 +443,24 @@ document.addEventListener('DOMContentLoaded', function() {
{
label: 'Kopi',
data: [0.5, 0.7, 1.1, 1.5],
borderColor: '#6F4E37',
backgroundColor: '#6F4E37',
borderColor: pastelCategory.coffee || '#6FD68F',
backgroundColor: pastelCategory.coffee || '#6FD68F',
tension: 0.3,
borderWidth: 2
},
{
label: 'Vanili',
data: [0.1, 0.15, 0.3, 0.5],
borderColor: '#eab308',
backgroundColor: '#eab308',
borderColor: pastelCategory.vanilla || '#FFB46F',
backgroundColor: pastelCategory.vanilla || '#FFB46F',
tension: 0.3,
borderWidth: 2
},
{
label: 'Kakao',
data: [0.4, 0.5, 0.8, 1.2],
borderColor: '#D2691E',
backgroundColor: '#D2691E',
borderColor: pastelCategory.cacao || '#9D8EF0',
backgroundColor: pastelCategory.cacao || '#9D8EF0',
tension: 0.3,
borderWidth: 2
}
@@ -467,7 +470,7 @@ document.addEventListener('DOMContentLoaded', function() {
responsive: true, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: '#f3f4f6' } },
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: gridColor } },
x: { grid: { display: false } }
},
plugins: {
@@ -489,11 +492,11 @@ document.addEventListener('DOMContentLoaded', function() {
label: 'Jumlah Bibit Didistribusikan',
data: [15000, 5200, 8000, 7000, 10000],
backgroundColor: [
'rgba(111, 78, 55, 0.8)', // Kopi
'rgba(234, 179, 8, 0.8)', // Vanili
'rgba(210, 105, 30, 0.8)', // Kakao
'rgba(156, 163, 175, 0.8)', // Kemiri
'rgba(34, 197, 94, 0.8)' // Pelindung (Hijau)
'rgba(111, 214, 143, 0.86)', // Kopi
'rgba(255, 180, 111, 0.86)', // Vanili
'rgba(157, 142, 240, 0.86)', // Kakao
'rgba(179, 181, 188, 0.86)', // Kemiri
'rgba(101, 216, 189, 0.86)' // Pelindung
],
borderRadius: 4
}
@@ -502,7 +505,7 @@ document.addEventListener('DOMContentLoaded', function() {
options: {
responsive: true, maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, grid: { color: '#f3f4f6' } },
y: { beginAtZero: true, grid: { color: gridColor } },
x: { grid: { display: false } }
},
plugins: {
@@ -515,4 +518,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
@endpush
@endpush
+26 -17
View File
@@ -219,6 +219,10 @@
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const pastelChart = config.chartColors || {};
const pastelSemantic = pastelChart.semantic || {};
const pastelCategory = pastelChart.categories || {};
const gridColor = pastelChart.grid || '#E8EDF5';
// Inisialisasi Icons (Khusus Mockup, Vuexy mungkin pakai library icon sendiri seperti Tabler/FontAwesome)
if(typeof lucide !== 'undefined') {
@@ -233,23 +237,23 @@
data: {
labels: ['2021', '2022', '2023', '2024'],
datasets: [
{
label: 'Baseline (Tanpa Intervensi)',
data: [12, 12.5, 13, 13.5],
borderColor: '#9ca3af',
backgroundColor: 'transparent',
{
label: 'Baseline (Tanpa Intervensi)',
data: [12, 12.5, 13, 13.5],
borderColor: pastelSemantic.gray || '#9EA6B0',
backgroundColor: 'transparent',
borderDash: [5, 5],
fill: false,
tension: 0.4
fill: false,
tension: 0.4
},
{
label: 'Pendapatan Aktual',
data: [12, 15, 22, 31],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.15)', // Light green fill
fill: true,
{
label: 'Pendapatan Aktual',
data: [12, 15, 22, 31],
borderColor: pastelCategory.marketGreen || '#6FD68F',
backgroundColor: 'rgba(111, 214, 143, 0.20)',
fill: true,
tension: 0.4,
pointBackgroundColor: '#10b981',
pointBackgroundColor: pastelCategory.marketGreen || '#6FD68F',
pointBorderWidth: 2,
pointRadius: 4
}
@@ -265,7 +269,7 @@
scales: {
y: {
beginAtZero: false,
grid: { color: '#f3f4f6' },
grid: { color: gridColor },
ticks: { callback: function(val) { return 'Rp ' + val + 'M'; } }
},
x: {
@@ -296,7 +300,12 @@
labels: ['Kopi', 'Vanili', 'Kakao', 'Kemiri'],
datasets: [{
data: [45, 25, 15, 15],
backgroundColor: ['#6F4E37', '#eab308', '#D2691E', '#9ca3af'],
backgroundColor: [
pastelCategory.coffee || '#6FD68F',
pastelCategory.vanilla || '#FFB46F',
pastelCategory.cacao || '#9D8EF0',
pastelCategory.candlenut || '#9EA6B0'
],
borderWidth: 2,
borderColor: '#ffffff',
hoverOffset: 4
@@ -324,4 +333,4 @@
}
});
</script>
@endpush
@endpush
@@ -12,9 +12,9 @@
box-shadow: 0 .25rem 1rem rgba(34, 48, 62, .08);
}
.tk-stat.green { border-left-color: #28a745; }
.tk-stat.blue { border-left-color: #007bff; }
.tk-stat.amber { border-left-color: #fd7e14; }
.tk-stat.green { border-left-color: #6FD68F; }
.tk-stat.blue { border-left-color: #5FC7D6; }
.tk-stat.amber { border-left-color: #FFB46F; }
.tk-stat-lbl {
font-size: 10px;
@@ -444,10 +444,10 @@
@forelse ($progressPetani as $petani)
@php
$barColor = match (true) {
$petani['percent'] === 100 => '#28a745',
$petani['percent'] >= 50 => '#007bff',
$petani['percent'] >= 20 => '#fd7e14',
default => '#dc3545',
$petani['percent'] === 100 => '#6FD68F',
$petani['percent'] >= 50 => '#5FC7D6',
$petani['percent'] >= 20 => '#FFB46F',
default => '#F47C86',
};
$initial = collect(explode(' ', trim($petani['name'])))
@@ -465,8 +465,8 @@
style="
width: 28px;
height: 28px;
background: rgba(40,167,69,.15);
color: #28a745;
background: rgba(111,214,143,.24);
color: #6FD68F;
"
>
{{ $initial ?: 'PT' }}
+2 -2
View File
@@ -43,7 +43,7 @@
<link rel="stylesheet" href="{{ asset('assets/vendor/libs/pickr/pickr-themes.css') }}" />
<link rel="stylesheet" href="{{ asset('assets/vendor/css/core.css') }}" />
<link rel="stylesheet" href="{{ asset('assets/css/demo.css') }}" />
<link rel="stylesheet" href="{{ asset('assets/css/demo.css') }}?v={{ filemtime(public_path('assets/css/demo.css')) }}" />
<link rel="stylesheet" href="{{ asset('assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css') }}" />
@@ -90,6 +90,6 @@
<script src="{{ asset('assets/vendor/js/template-customizer.js') }}"></script>
<script src="{{ asset('assets/js/config.js') }}"></script>
<script src="{{ asset('assets/js/config.js') }}?v={{ filemtime(public_path('assets/js/config.js')) }}"></script>
@stack('scripts-head')
</head>
+118 -131
View File
@@ -19,73 +19,69 @@
<div class="menu-inner-shadow"></div>
<ul class="menu-inner py-1">
<!-- Dashboards -->
@php
$userRole = auth()->check() ? (auth()->user()->role instanceof \App\Enums\Role ? auth()->user()->role->value : auth()->user()->role) : null;
$user = auth()->user();
$userRole = $user?->role instanceof \App\Enums\Role ? $user->role->value : $user?->role;
$appName = env('APP_NAME');
$isAdmin = in_array($userRole, ['super_admin', 'admin'], true);
$isSuperAdmin = $userRole === 'super_admin';
$canViewDashboard = $isAdmin || $userRole === 'fasilitator';
@endphp
@if(in_array($userRole, ['super_admin', 'admin', 'fasilitator']) )
<li class="menu-item active open">
@if($canViewDashboard)
<li class="menu-item {{ request()->routeIs('dashboard.*') ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-smart-home"></i>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-dashboard"></i></span>
<div data-i18n="Dashboard">Dashboard</div>
</a>
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroMonev')
@if($isAdmin && $appName === 'AgroMonev')
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs(['dashboard.karakteristik-petani', 'dashboard.demografi-petani', 'dashboard.indeks-petani']) ? 'active' : '' }}">
<a href="{{ route('dashboard.karakteristik-petani') }}" class="menu-link">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-user-circle"></i></span><div data-i18n="Karakteristik Petani">Karakteristik Petani</div>
</a>
</li>
</ul>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.produktivitas') ? 'active' : '' }}">
<a href="{{ route('dashboard.produktivitas') }}" class="menu-link">
<div data-i18n="Produktivitas">Produktivitas</div>
</a>
</li>
</ul>
@endif
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroMonev')
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.indeks-petani') ? 'active' : '' }}">
<a href="{{ route('dashboard.indeks-petani') }}" class="menu-link">
<div data-i18n="Indeks Petani">Indeks Petani</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>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.demografi-petani') ? 'active' : '' }}">
<a href="{{ route('dashboard.demografi-petani') }}" class="menu-link">
<div data-i18n="Demografi Petani">Demografi Petani</div>
<li class="menu-item {{ request()->routeIs('dashboard.pasar') ? 'active' : '' }}">
<a href="{{ route('dashboard.pasar') }}" class="menu-link">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-building-store"></i></span><div data-i18n="Pasar">Pasar</div>
</a>
</li>
</ul>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.indikator-pertanian') ? 'active' : '' }}">
<a href="{{ route('dashboard.indikator-pertanian') }}" class="menu-link">
<div data-i18n="Indikator Pertanian">Indikator Pertanian</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-chart-line"></i></span><div data-i18n="Indikator Petani">Indikator Petani</div>
</a>
</li>
</ul>
@endif
@if(in_array($userRole, ['super_admin', 'admin', 'fasilitator']) && env('APP_NAME') == 'AgroMonev')
@if($canViewDashboard && $appName === 'AgroMonev')
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.survey-petani') ? 'active' : '' }}">
<a href="{{ route('dashboard.survey-petani') }}" class="menu-link">
<div data-i18n="Survey Petani">Survey Petani</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-clipboard-list"></i></span><div data-i18n="Survei Petani">Survei Petani</div>
</a>
</li>
</ul>
<!-- <ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.petani') ? 'active' : '' }}">
<a href="{{ route('dashboard.petani') }}" class="menu-link">
<div data-i18n="Profil Petani">Profil Petani</div>
</a>
</li>
</ul> -->
@endif
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroMonev-xx')
@if($isAdmin && $appName === 'AgroMonev-xx')
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('dashboard.bumdes') ? 'active' : '' }}">
<a href="{{ route('dashboard.bumdes') }}" class="menu-link">
<div data-i18n="Profil Pengurus BUMDes">Profil Pengurus BUMDes</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-building-community"></i></span><div data-i18n="Profil Pengurus BUMDes">Profil Pengurus BUMDes</div>
</a>
</li>
</ul>
@@ -93,211 +89,202 @@
</li>
@endif
@if(in_array($userRole, ['fasilitator', 'super_admin', 'admin']) && env('APP_NAME') == 'AgroMonev')
<li class="menu-header small">
<span class="menu-header-text" data-i18n="Data Pertanian">Data Pertanian</span>
</li>
@if($canViewDashboard && $appName === 'AgroMonev')
<li class="menu-item {{ request()->routeIs(['petani.*', 'kelompok-tani.*', 'bumdes.*', 'koperasi.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-id-badge-2"></i></span>
<div data-i18n="Profil">Profil</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('petani.*') ? 'active' : '' }}">
<a href="{{ route('petani.index') }}" class="menu-link">
<i class="menu-icon icon-base ti tabler-file-description"></i>
<div data-i18n="Profil Petani">Profil Petani</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-user-circle"></i></span>
<div data-i18n="Petani">Petani</div>
</a>
</li>
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroMonev')
@if($isAdmin && $appName === 'AgroMonev')
<li class="menu-item {{ request()->routeIs('kelompok-tani.*') ? 'active' : '' }}">
<a href="{{ route('kelompok-tani.index') }}" class="menu-link">
<i class="menu-icon icon-base ti tabler-file-description"></i>
<div data-i18n="Profil Kelompok Tani">Profil Kelompok Tani</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-users-group"></i></span>
<div data-i18n="Kelompok Tani">Kelompok Tani</div>
</a>
</li>
<li class="menu-item {{ request()->routeIs('bumdes.*') ? 'active' : '' }}">
<a href="{{ route('bumdes.index') }}" class="menu-link">
<i class="menu-icon icon-base ti tabler-file-description"></i>
<div data-i18n="Profil BUMDes">Profil BUMDes</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-building-community"></i></span>
<div data-i18n="BUMDes">BUMDes</div>
</a>
</li>
<li class="menu-item {{ request()->routeIs('koperasi.*') ? 'active' : '' }}">
<a href="{{ route('koperasi.pengurus.index') }}" class="menu-link">
<i class="menu-icon icon-base ti tabler-file-description"></i>
<div data-i18n="Profil Pengurus Koperasi">Profil Pengurus Koperasi</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-heart-handshake"></i></span>
<div data-i18n="Pengurus Koperasi">Pengurus Koperasi</div>
</a>
</li>
@endif
@endif
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroPro')
<!-- Ekosistem -->
<li class="menu-header small">
<span class="menu-header-text" data-i18n="Ekosistem">Ekosistem</span>
</ul>
</li>
<li class="menu-item {{ request()->routeIs(['profile.*', 'produksi.*', 'lahan.*', 'tanaman.*']) ? 'active open' : '' }}">
@endif
@if($isAdmin && $appName === 'AgroPro')
<li class="menu-item {{ request()->routeIs(['petani.*', 'profile.*', 'produksi.*', 'lahan.*', 'tanaman.*', 'petani-cpm.*', 'lahan-cpcl.*', 'tabel.data.*', 'realisasi.*', 'kelompok-belajar-tani.*', 'workshop.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-file-description"></i>
<div data-i18n="Data Pertanian">Data Pertanian</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-layers-linked"></i></span>
<div data-i18n="Ekosistem">Ekosistem</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs(['petani.*', 'profile.*', 'produksi.*', 'lahan.*', 'tanaman.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-id-badge-2"></i></span>
<div data-i18n="Profil">Profil</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('petani.*') ? 'active' : '' }}">
<a href="{{ route('petani.index') }}" class="menu-link"><div data-i18n="Profil Petani">Profil Petani</div></a>
<a href="{{ route('petani.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-user-circle"></i></span><div data-i18n="Petani">Petani</div></a>
</li>
<li class="menu-item {{ request()->routeIs('produksi.*') ? 'active' : '' }}">
<a href="{{ route('produksi.index') }}" class="menu-link"><div data-i18n="Hasil Panen">Hasil Panen</div></a>
<a href="{{ route('produksi.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-wheat"></i></span><div data-i18n="Hasil Panen">Hasil Panen</div></a>
</li>
<li class="menu-item {{ request()->routeIs('lahan.*') ? 'active' : '' }}">
<a href="{{ route('lahan.index') }}" class="menu-link"><div data-i18n="Pemantauan Lahan">Pemantauan Lahan</div></a>
<a href="{{ route('lahan.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-map"></i></span><div data-i18n="Pemantauan Lahan">Pemantauan Lahan</div></a>
</li>
<li class="menu-item {{ request()->routeIs('tanaman.*') ? 'active' : '' }}">
<a href="{{ route('tanaman.index') }}" class="menu-link"><div data-i18n="Pemantauan Tanaman">Pemantauan Tanaman</div></a>
<a href="{{ route('tanaman.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-plant"></i></span><div data-i18n="Pemantauan Tanaman">Pemantauan Tanaman</div></a>
</li>
</ul>
</li>
<li class="menu-item {{ request()->routeIs(['petani-cpm.*', 'lahan-cpcl.*', 'tabel.data.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-folder-root"></i>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-database"></i></span>
<div data-i18n="Basis Data Ekosistem">Basis Data Ekosistem</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('petani-cpm.*') ? 'active' : '' }}">
<a href="{{ route('petani-cpm.index') }}" class="menu-link"><div data-i18n="Petani CPM">Petani CPM</div></a>
<a href="{{ route('petani-cpm.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-users-group"></i></span><div data-i18n="Petani CPM">Petani CPM</div></a>
</li>
<li class="menu-item {{ request()->routeIs('lahan-cpcl.*') ? 'active' : '' }}">
<a href="{{ route('lahan-cpcl.index') }}" class="menu-link"><div data-i18n="Lahan CPCL">Lahan CPCL</div></a>
<a href="{{ route('lahan-cpcl.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-map-2"></i></span><div data-i18n="Lahan CPCL">Lahan CPCL</div></a>
</li>
</ul>
@foreach ($tabel_menu ?? [] as $tbl)
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('tabel.data.*') && request()->route('tabel') == $tbl->id ? 'active' : '' }}">
<a href="{{ route('tabel.data.index', $tbl->id) }}" class="menu-link">
<div data-i18n="{{ $tbl->menu }}">{{ $tbl->menu }}</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-table"></i></span><div data-i18n="{{ $tbl->menu }}">{{ $tbl->menu }}</div>
</a>
</li>
</ul>
@endforeach
</li>
<!-- <li class="menu-item">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-line-scan"></i>
<div data-i18n="Tracebility">Tracebility</div>
</a>
<ul class="menu-sub">
<li class="menu-item">
<a href="#" class="menu-link">
<div data-i18n="Manajemen Batch & Kode">Manajemen Batch & Kode</div>
</a>
</li>
</ul>
<ul class="menu-sub">
<li class="menu-item">
<a href="#" class="menu-link">
<div data-i18n="Penelusuran Product">Penelusuran Product</div>
</a>
</li>
</ul>
</li> -->
<li class="menu-item {{ request()->routeIs(['realisasi.*', 'kelompok-belajar-tani.*', 'workshop.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-webhook"></i>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-users-group"></i></span>
<div data-i18n="Program & Komunitas">Program & Komunitas</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('realisasi.*') ? 'active' : '' }}">
<a href="{{ route('realisasi.index') }}" class="menu-link"><div data-i18n="Capaian Program">Capaian Program</div></a>
<a href="{{ route('realisasi.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-target"></i></span><div data-i18n="Capaian Program">Capaian Program</div></a>
</li>
<li class="menu-item {{ request()->routeIs('kelompok-belajar-tani.*') ? 'active' : '' }}">
<a href="{{ route('kelompok-belajar-tani.index') }}" class="menu-link"><div data-i18n="Kelompok Belajar Petani">Kelompok Belajar Petani</div></a>
<a href="{{ route('kelompok-belajar-tani.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-school"></i></span><div data-i18n="Kelompok Belajar Petani">Kelompok Belajar Petani</div></a>
</li>
<li class="menu-item {{ request()->routeIs('workshop.*') ? 'active' : '' }}">
<a href="{{ route('workshop.index') }}" class="menu-link"><div data-i18n="Workshop & Lokakarya">Workshop & Lokakarya</div></a>
<a href="{{ route('workshop.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-presentation"></i></span><div data-i18n="Workshop & Lokakarya">Workshop & Lokakarya</div></a>
</li>
</ul>
</li>
</ul>
</li>
@endif
@if(in_array($userRole, ['super_admin']))
<!-- Master Data -->
<li class="menu-header small">
<span class="menu-header-text" data-i18n="Master Data">Master Data</span>
</li>
<li class="menu-item">
@if($isSuperAdmin)
<li class="menu-item {{ request()->routeIs(['provinsi.*', 'kabupatenkota.*', 'kecamatan.*', 'desakelurahan.*', 'adat.*', 'keluarga.*', 'komoditas.*', 'jenis-komoditas.*', 'varietas.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-world"></i>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-database"></i></span>
<div data-i18n="Master Data">Master Data</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs(['provinsi.*', 'kabupatenkota.*', 'kecamatan.*', 'desakelurahan.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-map-2"></i></span>
<div data-i18n="Wilayah">Wilayah</div>
</a>
<ul class="menu-sub">
<li class="menu-item"><a href="{{ route('provinsi.index') }}" class="menu-link"><div data-i18n="Provinsi">Provinsi</div></a></li>
<li class="menu-item"><a href="{{ route('kabupatenkota.index') }}" class="menu-link"><div data-i18n="Kabupaten Kota">Kabupaten Kota</div></a></li>
<li class="menu-item"><a href="{{ route('kecamatan.index') }}" class="menu-link"><div data-i18n="Kecamatan">Kecamatan</div></a></li>
<li class="menu-item"><a href="{{ route('desakelurahan.index') }}" class="menu-link"><div data-i18n="Desa Kelurahan">Desa Kelurahan</div></a></li>
<li class="menu-item"><a href="{{ route('provinsi.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-map"></i></span><div data-i18n="Provinsi">Provinsi</div></a></li>
<li class="menu-item"><a href="{{ route('kabupatenkota.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-building"></i></span><div data-i18n="Kabupaten Kota">Kabupaten Kota</div></a></li>
<li class="menu-item"><a href="{{ route('kecamatan.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-map-pin"></i></span><div data-i18n="Kecamatan">Kecamatan</div></a></li>
<li class="menu-item"><a href="{{ route('desakelurahan.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-home"></i></span><div data-i18n="Desa Kelurahan">Desa Kelurahan</div></a></li>
</ul>
</li>
<li class="menu-item">
<a href="{{ route('adat.index') }}" class="menu-link"><i class="menu-icon icon-base ti tabler-matrix"></i><div data-i18n="Adat">Adat</div></a>
<li class="menu-item {{ request()->routeIs('adat.*') ? 'active' : '' }}">
<a href="{{ route('adat.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-heart-handshake"></i></span><div data-i18n="Adat">Adat</div></a>
</li>
<li class="menu-item">
<a href="{{ route('keluarga.index') }}" class="menu-link"><i class="menu-icon icon-base ti tabler-sitemap"></i><div data-i18n="Keluarga">Keluarga</div></a>
<li class="menu-item {{ request()->routeIs('keluarga.*') ? 'active' : '' }}">
<a href="{{ route('keluarga.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-sitemap"></i></span><div data-i18n="Keluarga">Keluarga</div></a>
</li>
<li class="menu-item">
<li class="menu-item {{ request()->routeIs(['komoditas.*', 'jenis-komoditas.*', 'varietas.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-plant"></i><div data-i18n="Tanaman">Tanaman</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-plant"></i></span><div data-i18n="Tanaman">Tanaman</div>
</a>
<ul class="menu-sub">
<li class="menu-item"><a href="{{ route('komoditas.index') }}" class="menu-link"><div data-i18n="Komoditas">Komoditas</div></a></li>
<li class="menu-item"><a href="{{ route('jenis-komoditas.index') }}" class="menu-link"><div data-i18n="Jenis Komoditas">Jenis Komoditas</div></a></li>
<li class="menu-item"><a href="{{ route('varietas.index') }}" class="menu-link"><div data-i18n="Varietas">Varietas</div></a></li>
<li class="menu-item"><a href="{{ route('komoditas.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-plant"></i></span><div data-i18n="Komoditas">Komoditas</div></a></li>
<li class="menu-item"><a href="{{ route('jenis-komoditas.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-layout-grid"></i></span><div data-i18n="Jenis Komoditas">Jenis Komoditas</div></a></li>
<li class="menu-item"><a href="{{ route('varietas.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-leaf"></i></span><div data-i18n="Varietas">Varietas</div></a></li>
</ul>
</li>
</ul>
</li>
@endif
@if(in_array($userRole, ['super_admin', 'admin']))
<li class="menu-header small">
<span class="menu-header-text" data-i18n="Konfigurasi">Konfigurasi</span>
</li>
@if($isAdmin)
<li class="menu-item {{ request()->routeIs(['users.*', 'program.*', 'framework.*', 'group.*']) || (request()->routeIs('tabel.*') && !request()->routeIs('tabel.data.*')) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-settings"></i></span>
<div data-i18n="Konfigurasi">Konfigurasi</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('users.*') ? 'active' : '' }}">
<a href="{{ route('users.index') }}" class="menu-link">
<i class="menu-icon icon-base ti tabler-users"></i><div data-i18n="Daftar Pengguna">Daftar Pengguna</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-user-shield"></i></span><div data-i18n="Daftar Pengguna">Daftar Pengguna</div>
</a>
</li>
@endif
@if(in_array($userRole, ['super_admin', 'admin']) && env('APP_NAME') == 'AgroPro')
@if($isAdmin && $appName === 'AgroPro')
<li class="menu-item {{ request()->routeIs(['program.*', 'framework.*']) ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-transform"></i><div data-i18n="Program Transformasi">Program Transformasi</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-route"></i></span><div data-i18n="Program Transformasi">Program Transformasi</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('program.*') ? 'active' : '' }}">
<a href="{{ route('program.index') }}" class="menu-link"><div data-i18n="Program Transformasi">Program Transformasi</div></a>
<a href="{{ route('program.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-route"></i></span><div data-i18n="Program Transformasi">Program Transformasi</div></a>
</li>
<li class="menu-item {{ request()->routeIs('framework.*') ? 'active' : '' }}">
<a href="{{ route('framework.index') }}" class="menu-link"><div data-i18n="Framework">Framework</div></a>
<a href="{{ route('framework.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-sitemap"></i></span><div data-i18n="Framework">Framework</div></a>
</li>
</ul>
</li>
<li class="menu-item {{ request()->routeIs(['group.*', 'tabel.*']) && !request()->routeIs('tabel.data.*') ? 'active open' : '' }}">
<a href="javascript:void(0);" class="menu-link menu-toggle">
<i class="menu-icon icon-base ti tabler-database"></i><div data-i18n="Basis Data Ekosistem">Basis Data Ekosistem</div>
<span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-database"></i></span><div data-i18n="Basis Data Ekosistem">Basis Data Ekosistem</div>
</a>
<ul class="menu-sub">
<li class="menu-item {{ request()->routeIs('group.*') ? 'active' : '' }}">
<a href="{{ route('group.index') }}" class="menu-link"><div data-i18n="Kelompok">Kelompok</div></a>
<a href="{{ route('group.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-users-group"></i></span><div data-i18n="Kelompok">Kelompok</div></a>
</li>
<li class="menu-item {{ request()->routeIs('tabel.*') && !request()->routeIs('tabel.data.*') ? 'active' : '' }}">
<a href="{{ route('tabel.index') }}" class="menu-link"><div data-i18n="Tabel">Tabel</div></a>
<a href="{{ route('tabel.index') }}" class="menu-link"><span class="menu-icon sidebar-icon" aria-hidden="true"><i class="icon-base ti tabler-table"></i></span><div data-i18n="Tabel">Tabel</div></a>
</li>
</ul>
</li>
@endif
<!-- Misc -->
<!-- <li class="menu-header small">
<span class="menu-header-text" data-i18n="Misc">Misc</span>
</ul>
</li>
<li class="menu-item">
<a href="https://docs.google.com/spreadsheets/d/1nbh4xW-K_GwRW34-cD81p9B-LnLvkvro/edit?usp=sharing&ouid=103442063105125817060&rtpof=true&sd=true" target="_blank" class="menu-link">
<i class="menu-icon icon-base ti tabler-file-description"></i><div data-i18n="Documentation">Documentation</div>
</a>
</li> -->
@endif
</ul>
</aside>
@@ -234,7 +234,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Profil</button>
</div>
@@ -30,11 +30,110 @@
<div class="card mb-6">
<div class="card-body pt-12">
<h5 class="pb-3 border-bottom mb-4">Pengalaman Saluran Lain</h5>
<h5 class="pb-4 border-bottom mb-4">Saluran Penjualan & Akses Pasar</h5>
<h5 class="mb-4 mt-4">Saluran Penjualan Utama</h5>
<div class="mb-4">
<p class="form-label d-block">Kepada siapa Bapak/Ibu biasanya menjual hasil panen? (boleh pilih lebih dari satu)</p>
<div class="row">
@php
$tujuanOptions = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir'];
@endphp
@foreach($tujuanOptions as $tujuan)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="{{ $tujuan }}" id="tujuan-{{ Str::slug($tujuan) }}" {{ in_array($tujuan, (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="tujuan-{{ Str::slug($tujuan) }}">{{ $tujuan }}</label>
</div>
</div>
@endforeach
<div class="col-12 mt-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" name="tujuan_penjualan[]" value="Lainnya" id="tujuan-lainnya" {{ in_array('Lainnya', (array) old('tujuan_penjualan', $aksesPasar?->tujuan_penjualan ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="tujuan-lainnya">Lainnya:</label>
</div>
<input type="text" class="form-control w-auto flex-grow-1 max-w-400" style="max-width: 400px;" name="tujuan_penjualan_lainnya" value="{{ old('tujuan_penjualan_lainnya', $aksesPasar?->tujuan_penjualan_lainnya) }}" placeholder="Sebutkan lainnya...">
</div>
</div>
</div>
</div>
<h5 class="mb-4 mt-4">Sifat Hubungan Dengan Pembeli</h5>
<div class="mb-4">
<p class="form-label d-block">Bagaimana sifat hubungan jual Bapak/Ibu dengan pembeli utama saat ini?</p>
<div class="row">
@php
$sifatHubunganOptions = [
'Bebas, bisa pindah pembeli kapan saja',
'Langganan tetap, tapi tidak terikat',
'Berbasis hutang, harus jual ke sana karena ada pinjaman',
'Titip jual,dibayar setelah produk terjual',
'Ada komitmen volume, sudah ada kesepakatan jumlah',
'Semi-kontrak, ada kesepakatan harga tertentu'
];
@endphp
@foreach($sifatHubunganOptions as $sifat)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="sifat_hubungan_bisnis" value="{{ $sifat }}" id="sifat-{{ Str::slug($sifat) }}" {{ old('sifat_hubungan_bisnis', $aksesPasar?->sifat_hubungan_bisnis) === $sifat ? 'checked' : '' }}>
<label class="form-check-label" for="sifat-{{ Str::slug($sifat) }}">{{ $sifat }}</label>
</div>
</div>
@endforeach
</div>
</div>
<h5 class="mb-4 mt-4">Bentuk Produk Yang Dijual</h5>
<div class="mb-4">
<p class="form-label d-block">Dalam bentuk apa produk dijual?</p>
<div class="row">
@php
$bentukPenjualanOptions = [
'Buah segar hasil petik (gelondongan)',
'Biji/polong basah (belum dikeringkan)',
'Biji/polong kering/biji kupas',
'Terfermentasi'
];
@endphp
@foreach($bentukPenjualanOptions as $bentuk)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="bentuk_penjualan" value="{{ $bentuk }}" id="bentuk-{{ Str::slug($bentuk) }}" {{ old('bentuk_penjualan', $aksesPasar?->bentuk_penjualan) === $bentuk ? 'checked' : '' }}>
<label class="form-check-label" for="bentuk-{{ Str::slug($bentuk) }}">{{ $bentuk }}</label>
</div>
</div>
@endforeach
</div>
</div>
<h5 class="mb-4 mt-4">Frekuensi Penjualan</h5>
<div class="mb-4">
<p class="form-label d-block">Kapan biasanya menjual hasil panen?</p>
<div class="d-flex flex-column">
@php
$frekuensiOptions = [
'Langsung setelah panen',
'Ditumpuk dulu beberapa waktu',
'Saat butuh uang mendesak'
];
@endphp
@foreach($frekuensiOptions as $freq)
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="frekuensi_penjualan" value="{{ $freq }}" id="freq-{{ Str::slug($freq) }}" {{ old('frekuensi_penjualan', $aksesPasar?->frekuensi_penjualan) === $freq ? 'checked' : '' }}>
<label class="form-check-label" for="freq-{{ Str::slug($freq) }}">{{ $freq }}</label>
</div>
@endforeach
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Pengalaman Saluran Lain</h5>
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block fw-bold">Pengalaman & akses ke saluran lain</label>
<div class="col-md-12 mb-4 form-control-validation">
<label class="form-label d-block">Apakah pernah menjual ke tempat lain sebelumnya?</label>
@foreach($skorPengalamanOptions as $option)
<div class="form-check mb-2">
<input class="form-check-input @error('skor_pengalaman') is-invalid @enderror" type="radio"
@@ -46,41 +145,67 @@
@error('skor_pengalaman')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="alasan-berhenti">Alasan berhenti</label>
<input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
@error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="saluran-terdahulu">Dulu pernah jual ke?</label>
@foreach($saluranTerdahuluOptions as $option)
<div class="form-check mb-2">
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
name="saluran_terdahulu[]"
value="{{ $option['value'] }}"
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
{{ $option['label'] }}
</label>
<div class="col-md-12" id="blok-saluran-lanjutan">
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label" for="alasan-berhenti">Alasan berhenti</label>
<input type="text" class="form-control @error('alasan_berhenti') is-invalid @enderror" id="alasan-berhenti" name="alasan_berhenti" value="{{ old('alasan_berhenti', $aksesPasar?->alasan_berhenti) }}" placeholder="Opsional (jika sudah berhenti)...">
@error('alasan_berhenti')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
@endforeach
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
<div class="col-md-4 mb-4 form-control-validation">
<label class="form-label" for="saluran-lainnya">Lainnya</label>
<input type="text" class="form-control @error('saluran_terdahulu_lainnya') is-invalid @enderror" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya...">
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback">{{ $message }}</div>@enderror
<div class="col-md-6 mb-4"></div>
<div class="col-md-12 mb-4 form-control-validation">
<label class="form-label d-block" for="saluran-terdahulu">Jika pernah, dulu menjual ke mana? (boleh pilih lebih dari satu)</label>
<div class="row">
@foreach($saluranTerdahuluOptions as $option)
@if($option['value'] !== $saluranLainnyaValue)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-{{ Str::slug($option['value']) }}"
name="saluran_terdahulu[]"
value="{{ $option['value'] }}"
{{ in_array($option['value'], (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="saluran-terdahulu-{{ Str::slug($option['value']) }}">
{{ $option['label'] }}
</label>
</div>
</div>
@endif
@endforeach
<div class="col-12 mt-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="form-check mb-0">
<input class="form-check-input @error('saluran_terdahulu') is-invalid @enderror"
type="checkbox"
id="saluran-terdahulu-lainnya-checkbox"
name="saluran_terdahulu[]"
value="{{ $saluranLainnyaValue }}"
{{ in_array($saluranLainnyaValue, (array) old('saluran_terdahulu', $aksesPasar?->saluran_terdahulu ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label text-nowrap" for="saluran-terdahulu-lainnya-checkbox">
Lainnya:
</label>
</div>
<input type="text" class="form-control w-auto flex-grow-1 @error('saluran_terdahulu_lainnya') is-invalid @enderror" style="max-width: 400px;" id="saluran-lainnya" name="saluran_terdahulu_lainnya" value="{{ old('saluran_terdahulu_lainnya', $aksesPasar?->saluran_terdahulu_lainnya) }}" placeholder="Sebutkan saluran lainnya...">
</div>
</div>
</div>
@error('saluran_terdahulu')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
@error('saluran_terdahulu_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
</div>
</div>
</div>
<h5 class="pb-3 border-bottom mb-4 mt-4">HARGA DI SETIAP TITIK</h5>
<h6 class="mb-2">Harga Jual Panen Terakhir</h6>
<p class="mb-1">Harga jual komoditas tiga musim panen terakhir:</p>
<p class="mb-4">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p>
<h5 class="pb-4 border-bottom mb-4 mt-4">Harga Di Setiap Titik</h5>
<h5 class="mb-4 mt-4">Harga Jual Panen Terakhir</h5>
<p class="form-label d-block">Harga jual komoditas tiga musim panen terakhir:</p>
<p class="form-label d-block">Dalam tiga musim panen terakhir, berapa harga jual yang Bapak/Ibu terima untuk komoditas ini sesuai dengan bentuk produk yang dijual?</p>
<div class="table-responsive mb-4">
<table class="table table-bordered align-middle">
@@ -97,7 +222,7 @@
<tr>
<td>
<input type="hidden" name="harga_jual[{{ $row['index'] }}][bentuk_produk]" value="{{ $row['bentuk'] }}">
<strong>{{ $row['bentuk'] }}:</strong>
{{ $row['bentuk'] }}:
</td>
<td>
<div class="input-group">
@@ -126,18 +251,87 @@
</table>
</div>
<h5 class="pb-3 border-bottom mb-4 mt-4">PELUANG NILAI TAMBAH</h5>
<div class="border rounded p-4 mb-4">
<h5 class="mb-4 mt-4">Sumber Informasi Harga</h5>
<div class="mb-4">
<div class="mb-4">
<h6 class="mb-1 text-primary">Detail Praktik Fermentasi</h6>
<p class="text-muted small mb-0">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p>
<p class="form-label d-block">Sebelum menjual, apakah Bapak/Ibu sudah tahu harga yang akan diterima?</p>
<div class="row">
@php
$tahuHargaOptions = [
'Selalu tahu sebelum pergi jual',
'Kadang tahu, kadang tidak',
'Baru tahu saat sudah di tempat pembeli',
'Tidak pernah tahu sebelumnya — langsung jual saja'
];
@endphp
@foreach($tahuHargaOptions as $tahu)
<div class="col-md-6">
<div class="form-check mb-2">
<input class="form-check-input @error('tahu_harga_sebelumnya') is-invalid @enderror" type="radio" name="tahu_harga_sebelumnya" value="{{ $tahu }}" id="tahu-{{ Str::slug($tahu) }}" {{ old('tahu_harga_sebelumnya', $aksesPasar?->tahu_harga_sebelumnya) === $tahu ? 'checked' : '' }}>
<label class="form-check-label" for="tahu-{{ Str::slug($tahu) }}">{{ $tahu }}</label>
</div>
</div>
@endforeach
</div>
@error('tahu_harga_sebelumnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
<div class="mb-0">
<p class="form-label d-block">Dari mana biasanya Bapak/Ibu tahu harga hasil panen sebelum menjual? (boleh pilih lebih dari satu)</p>
<div class="row">
@php
$sumberInfoHargaOptions = [
'Tengkulak/pedagang yang datang langsung',
'Sesama petani/tetangga',
'PPL/petugas dinas',
'HP/WhatsApp/internet',
'Radio/TV',
'Facebook',
'Tiktok',
'Instagram',
'Tidak pernah cek — langsung jual saja'
];
$sumberInfoLainnyaValue = 'Lainnya';
@endphp
@foreach($sumberInfoHargaOptions as $sumber)
<div class="col-md-4">
<div class="form-check mb-2">
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumber }}" id="sumber-{{ Str::slug($sumber) }}" {{ in_array($sumber, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label" for="sumber-{{ Str::slug($sumber) }}">{{ $sumber }}</label>
</div>
</div>
@endforeach
<div class="col-12 mt-2">
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="form-check mb-0">
<input class="form-check-input @error('sumber_info_harga') is-invalid @enderror" type="checkbox" name="sumber_info_harga[]" value="{{ $sumberInfoLainnyaValue }}" id="sumber-lainnya-checkbox" {{ in_array($sumberInfoLainnyaValue, (array) old('sumber_info_harga', $aksesPasar?->sumber_info_harga ?? []), true) ? 'checked' : '' }}>
<label class="form-check-label text-nowrap" for="sumber-lainnya-checkbox">Lainnya:</label>
</div>
<input type="text" class="form-control w-auto flex-grow-1 @error('sumber_info_harga_lainnya') is-invalid @enderror" style="max-width: 400px;" id="sumber-info-lainnya-input" name="sumber_info_harga_lainnya" value="{{ old('sumber_info_harga_lainnya', $aksesPasar?->sumber_info_harga_lainnya) }}" placeholder="Sebutkan sumber lainnya...">
</div>
</div>
</div>
@error('sumber_info_harga')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
@error('sumber_info_harga_lainnya')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-4 border-bottom mb-4 mt-4">Peluang Nilai Tambah</h5>
<div class="mb-4">
<div class="mb-4">
<h5 class="mb-4 mt-4">Detail Praktik Fermentasi</h5>
<p class="form-label d-block">Apakah sudah ada praktik fermentasi atau pengolahan pascapanen yang Ibu/Bapak lakukan?</p>
</div>
<div class="row">
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end">
<label class="form-label mb-0" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label>
<div class="d-flex align-items-end">
<label class="form-label" for="praktik-fermentasi">Praktik fermentasi yang dilakukan</label>
</div>
<div>
@foreach($praktikFermentasiOptions as $option)
@@ -158,8 +352,8 @@
</div>
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end">
<label class="form-label mb-0" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label>
<div class="d-flex align-items-end">
<label class="form-label" for="proses-fermentasi">Jika ya, bagaimana prosesnya?</label>
</div>
<div>
<textarea class="form-control @error('proses_fermentasi') is-invalid @enderror" id="proses-fermentasi" name="proses_fermentasi" rows="2" placeholder="Ceritakan prosesnya di sini...">{{ old('proses_fermentasi', $aksesPasar?->proses_fermentasi) }}</textarea>
@@ -168,8 +362,8 @@
</div>
<div class="col-md-4 mb-4 form-control-validation d-flex flex-column">
<div class="mb-2 d-flex align-items-end">
<label class="form-label mb-0" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label>
<div class="d-flex align-items-end">
<label class="form-label" for="model-fermentasi">Menurut Bapak/Ibu, apa model yang paling memungkinkan?</label>
</div>
<div>
<textarea class="form-control @error('model_fermentasi_memungkinkan') is-invalid @enderror" id="model-fermentasi" name="model_fermentasi_memungkinkan" rows="2" placeholder="Model fermentasinya...">{{ old('model_fermentasi_memungkinkan', $aksesPasar?->model_fermentasi_memungkinkan) }}</textarea>
@@ -179,11 +373,11 @@
</div>
</div>
<h5 class="pb-3 border-bottom mb-4 mt-4">AKSES PASAR PREMIUM</h5>
<h6 class="mb-3">Kesadaran Pasar Premium*</h6>
<h5 class="pb-4 border-bottom mb-4 mt-4">Akses Pasar Premium</h5>
<h5 class="mb-4 mt-4">Kesadaran Pasar Premium*</h5>
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="skor-premium">Pengetahuan & akses pasar premium</label>
<label class="form-label d-block" for="skor-premium">Pengetahuan & akses pasar premium</label>
@foreach($skorKesadaranPremiumOptions as $option)
<div class="form-check mb-2">
<input class="form-check-input @error('skor_kesadaran_premium') is-invalid @enderror"
@@ -201,16 +395,16 @@
</div>
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label mb-2" for="sumber-info">Sumber informasi utama</label>
<label class="form-label" for="sumber-info">Sumber informasi utama</label>
<input type="text" class="form-control @error('sumber_info_premium') is-invalid @enderror" id="sumber-info" name="sumber_info_premium" value="{{ old('sumber_info_premium', $aksesPasar?->sumber_info_premium) }}">
@error('sumber_info_premium')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<h5 class="pb-3 border-bottom mb-4 mt-4">KEPERCAYAAN & PERSEPSI RISIKO</h5>
<h5 class="pb-4 border-bottom mb-4 mt-4">Kepercayaan & Persepsi Risiko</h5>
<div class="row">
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label d-block mb-2" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label>
<label class="form-label d-block" for="tingkat-kepercayaan">Seberapa percaya Bapak/Ibu untuk menjual ke pembeli atau pasar baru, di luar pembeli yang biasa?</label>
@foreach($tingkatKepercayaanOptions as $option)
<div class="form-check mb-2">
<input class="form-check-input @error('tingkat_kepercayaan_pembeli_baru') is-invalid @enderror"
@@ -228,7 +422,7 @@
</div>
<div class="col-md-6 mb-4 form-control-validation">
<label class="form-label mb-2" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label>
<label class="form-label" for="alasan-kepercayaan">Apa yang membuat Bapak/Ibu ragu atau yakin untuk mencoba pembeli baru?</label>
<textarea class="form-control @error('alasan_kepercayaan_pembeli_baru') is-invalid @enderror"
id="alasan-kepercayaan"
name="alasan_kepercayaan_pembeli_baru"
@@ -239,8 +433,9 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-device-floppy me-1_5 icon-sm"></i>Simpan Akses Pasar
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>Simpan
</button>
</div>
</div>
@@ -258,6 +453,8 @@
const saluranLainnyaValue = @json($saluranLainnyaValue);
const praktikBelumPernahValue = String(@json($praktikBelumPernahValue));
const skorPengalamanBerhentiValues = @json($skorPengalamanBerhentiValues ?? [2,3]).map(Number);
const skorBelumPernahValue = "1";
const sumberInfoLainnyaValue = @json($sumberInfoLainnyaValue ?? 'Lainnya');
function onlyDigits(value) {
return String(value || '').replace(/\D/g, '');
@@ -265,10 +462,25 @@
function formatRupiah(value) {
const digits = onlyDigits(value);
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
function toggleSaluranLanjutan() {
const selectedRadio = $('input[name="skor_pengalaman"]:checked');
const val = selectedRadio.val();
const labelText = selectedRadio.parent().text().toLowerCase();
const isBelumPernah = !val || labelText.includes('belum pernah') || val === skorBelumPernahValue;
if (isBelumPernah) {
$('#blok-saluran-lanjutan').addClass('d-none');
$('#blok-saluran-lanjutan input[type="text"]').val('');
$('#blok-saluran-lanjutan input[type="checkbox"]').prop('checked', false);
} else {
$('#blok-saluran-lanjutan').removeClass('d-none');
}
}
function syncAlasanBerhenti() {
const val = Number($('input[name="skor_pengalaman"]:checked').val());
const isBerhenti = skorPengalamanBerhentiValues.includes(val);
@@ -282,6 +494,12 @@
if (!isLainnya) $('#saluran-lainnya').val('');
}
function syncSumberInfoLainnya() {
const isLainnya = $('input[name="sumber_info_harga[]"][value="' + sumberInfoLainnyaValue + '"]').is(':checked');
$('#sumber-info-lainnya-input').prop('disabled', !isLainnya);
if (!isLainnya) $('#sumber-info-lainnya-input').val('');
}
function syncPraktikFermentasi() {
const isBelumPernah = $('input[name="praktik_fermentasi"]:checked').val() === praktikBelumPernahValue;
$('#proses-fermentasi, #model-fermentasi').prop('disabled', isBelumPernah);
@@ -290,9 +508,15 @@
}
}
$('input[name="skor_pengalaman"]').on('change', syncAlasanBerhenti);
$('input[name="skor_pengalaman"]').on('change', function () {
toggleSaluranLanjutan();
syncAlasanBerhenti();
});
$('input[name="saluran_terdahulu[]"]').on('change', syncSaluranLainnya);
$('input[name="sumber_info_harga[]"]').on('change', syncSumberInfoLainnya);
$('input[name="praktik_fermentasi"]').on('change', syncPraktikFermentasi);
$(document).on('input', '.rupiah-input', function () {
const cursorAtEnd = this.selectionStart === this.value.length;
this.value = formatRupiah(this.value);
@@ -301,12 +525,15 @@
this.setSelectionRange(this.value.length, this.value.length);
}
});
$('.rupiah-input').each(function () {
this.value = formatRupiah(this.value);
});
toggleSaluranLanjutan();
syncAlasanBerhenti();
syncSaluranLainnya();
syncSumberInfoLainnya();
syncPraktikFermentasi();
});
</script>
@@ -13,6 +13,7 @@
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
@@ -43,13 +44,18 @@
<div class="col-md-3">
<div class="user-avatar-section">
<div class=" d-flex align-items-center flex-column">
<img id="fotoPreview"
class="mb-3 shadow-sm rounded-3"
<button type="button" class="border-0 bg-transparent p-0 mb-3 rounded-3"
data-bs-toggle="modal" data-bs-target="#fotoProfilModal"
aria-label="Lihat foto profil petani" title="Lihat foto profil petani">
<img id="fotoPreview"
class="shadow-sm rounded-3"
src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/3.jpg') }}"
alt="Foto profil petani"
data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Klik untuk mengganti foto profil petani"
style="width: 120px !important; height: 120px !important; cursor:pointer; object-fit:cover; display:block; margin: 0 auto;" />
</button>
<button type="button" id="btnGantiFoto" class="btn btn-sm btn-outline-primary mb-3">
Ganti Foto
</button>
<button type="button" id="btnHapusFoto" class="btn btn-sm btn-outline-danger mb-4" style="display: {{ $petani->foto ? 'block' : 'none' }};">
Hapus Foto
@@ -113,7 +119,7 @@
<select class="form-control @error('pendidikan_terakhir') is-invalid @enderror" id="edit-petani-pendidikan-terakhir" name="pendidikan_terakhir">
<option value="">Pilih Pendidikan Terakhir</option>
@foreach($pendidikan as $value)
<option value="{{ $value['value'] }}" {{ old('pendidikan_terakhir', $petani->pendidikan_terakhir->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
<option value="{{ $value['value'] }}" {{ old('pendidikan_terakhir', $petani->pendidikan_terakhir?->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
@endforeach
</select>
@error('pendidikan_terakhir')<div class="error invalid-feedback">{{ $message }}</div>@enderror
@@ -123,7 +129,7 @@
<select class="form-control @error('status_perkawinan') is-invalid @enderror" id="edit-petani-status-pernikahan" name="status_perkawinan">
<option value="">Pilih Status Pernikahan</option>
@foreach($pernikahan as $value)
<option value="{{ $value['value'] }}" {{ old('status_perkawinan', $petani->status_perkawinan->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
<option value="{{ $value['value'] }}" {{ old('status_perkawinan', $petani->status_perkawinan?->value) == $value['value'] ? 'selected' : '' }}>{{ $value['label'] }}</option>
@endforeach
</select>
@error('status_perkawinan')<div class="error invalid-feedback">{{ $message }}</div>@enderror
@@ -159,7 +165,7 @@
<select class="form-control @error('provinsi_id') is-invalid @enderror" id="edit-petani-provinsi" name="provinsi_id">
<option value="">Pilih Provinsi</option>
@foreach($provinsi as $p)
<option value="{{ $p->id }}" {{ old('provinsi_id', $petani->desaKelurahan->kecamatan->kabupatenKota->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
<option value="{{ $p->id }}" {{ old('provinsi_id', $petani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id) == $p->id ? 'selected' : '' }}>{{ $p->nama }}</option>
@endforeach
</select>
@error('provinsi_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
@@ -168,7 +174,9 @@
<label class="form-label" for="edit-petani-kabupaten">Kabupaten/Kota<span class="text-danger">*</span></label>
<select class="form-control @error('kabupaten_id') is-invalid @enderror" id="edit-petani-kabupaten" name="kabupaten_id" >
<option value="">Pilih Kabupaten/Kota</option>
@if($petani->desaKelurahan?->kecamatan?->kabupatenKota)
<option value="{{ $petani->desaKelurahan->kecamatan->kabupatenKota->id }}" selected>{{ $petani->desaKelurahan->kecamatan->kabupatenKota->nama }}</option>
@endif
</select>
@error('kabupaten_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
@@ -176,7 +184,9 @@
<label class="form-label" for="edit-petani-kecamatan">Kecamatan<span class="text-danger">*</span></label>
<select class="form-control @error('kecamatan_id') is-invalid @enderror" id="edit-petani-kecamatan" name="kecamatan_id" >
<option value="">Pilih Kecamatan</option>
@if($petani->desaKelurahan?->kecamatan)
<option value="{{ $petani->desaKelurahan->kecamatan->id }}" selected>{{ $petani->desaKelurahan->kecamatan->nama }}</option>
@endif
</select>
@error('kecamatan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
@@ -184,7 +194,9 @@
<label class="form-label" for="edit-petani-desa">Desa/Kelurahan<span class="text-danger">*</span></label>
<select class="form-control @error('desa_kelurahan_id') is-invalid @enderror" id="edit-petani-desa" name="desa_kelurahan_id" >
<option value="">Pilih Desa/Kelurahan</option>
@if($petani->desaKelurahan)
<option value="{{ $petani->desaKelurahan->id }}" selected>{{ $petani->desaKelurahan->nama }}</option>
@endif
</select>
@error('desa_kelurahan_id')<div class="error invalid-feedback">{{ $message }}</div>@enderror
</div>
@@ -242,7 +254,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Profil</button>
</div>
@@ -253,6 +265,20 @@
</div>
</div>
<div class="modal fade" id="fotoProfilModal" tabindex="-1" aria-labelledby="fotoProfilModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="fotoProfilModalLabel">Foto Profil Petani</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Tutup"></button>
</div>
<div class="modal-body text-center">
<img id="fotoProfilBesar" class="img-fluid rounded-3" alt="Foto profil petani" style="max-height: 75vh; object-fit: contain;" />
</div>
</div>
</div>
</div>
<div class="modal fade" id="cropModal" tabindex="-1" aria-labelledby="cropModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
@@ -344,7 +370,11 @@
let croppieInstance = null;
$('#fotoPreview').on('click', function () {
$('#fotoProfilModal').on('show.bs.modal', function () {
$('#fotoProfilBesar').attr('src', $('#fotoPreview').attr('src'));
});
$('#btnGantiFoto').on('click', function () {
$('#foto').val('');
$('#foto').click();
});
@@ -58,7 +58,7 @@
<hr class="my-2"> <h6 class="mb-1" style="font-size: 0.85rem;">Domisili</h6>
<p class="mb-0 text-muted" style="font-size: 0.75rem;">Alamat: {{ isset($petani) ? $petani->alamat : 'Alamat' }}</p>
<p class="mb-0 text-muted" style="font-size: 0.75rem; line-height: 1.3;">{{ isset($petani) ? $petani->desaKelurahan->nama : 'Desa/Kelurahan' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->nama : 'Kecamatan' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->kabupatenKota->nama : 'Kabupaten/Kota' }}, {{ $petani ? $petani->desaKelurahan->kecamatan->kabupatenKota->provinsi->nama : 'Provinsi' }}</p>
<p class="mb-0 text-muted" style="font-size: 0.75rem; line-height: 1.3;">{{ isset($petani) ? $petani->desaKelurahan?->nama ?? 'Belum diisi' : 'Desa/Kelurahan' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->nama ?? 'Belum diisi' : 'Kecamatan' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->kabupatenKota?->nama ?? 'Belum diisi' : 'Kabupaten/Kota' }}, {{ $petani ? $petani->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi?->nama ?? 'Belum diisi' : 'Provinsi' }}</p>
</div>
</div>
</div>
@@ -116,7 +116,7 @@
</h5>
<p class="mb-0" style="font-size: 0.75rem;">
@if(!is_null($skorLahanTanaman))
<span class="me-1">{{ number_format($totalTanaman, 0, ',', '.') }} tanaman {{ $petani?->komoditas->nama }}</span>
<span class="me-1">{{ number_format($totalTanaman, 0, ',', '.') }} tanaman {{ $petani?->komoditas?->nama ?? 'Belum diisi' }}</span>
@else
<span class="me-1">Belum terisi</span>
@endif
@@ -1,44 +1,292 @@
@extends('layout.main')
@push('style')
<style>
#data-petani {
scroll-margin-top: 7rem;
}
.petani-row {
cursor: pointer;
}
.indicator-filter-row {
padding-bottom: 1.5rem;
padding-top: 1.5rem;
}
.indicator-filter-bar {
display: block;
}
.indicator-filter-line {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.indicator-filter-section-label {
align-items: center;
display: flex;
font-weight: 500;
margin-right: .5rem;
white-space: nowrap;
}
.indicator-filter-bar .form-select,
.indicator-filter-bar .btn {
min-height: 31px;
}
.indicator-filter-bar .form-select {
background-color: var(--bs-paper-bg);
border-color: var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
}
.indicator-filter-bar .btn {
white-space: nowrap;
}
.indicator-filter-bar .form-select:focus {
border-color: rgba(40, 167, 69, .55);
box-shadow: 0 0 0 .2rem rgba(40, 167, 69, .12);
}
.indicator-filter-static {
align-items: center;
background-color: var(--bs-paper-bg);
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
color: var(--bs-body-color);
display: flex;
font-size: .8125rem;
min-height: 31px;
padding: .25rem .75rem;
}
.indicator-filter-bar .input-group-sm {
width: 240px;
}
@media (max-width: 767.98px) {
.indicator-filter-line {
display: grid;
grid-template-columns: 1fr;
width: 100%;
}
.indicator-filter-section-label {
margin-right: 0;
}
.indicator-filter-bar .btn {
justify-content: center;
width: 100%;
}
}
</style>
@endpush
@section('content')
<div class="container-xxl flex-grow-1 container-p-y">
<div class="d-flex flex-row gap-3 mb-4">
<div class="w-25">
<div class="container-fluid flex-grow-1 container-p-y">
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<x-summary-card
title="Petani"
count="{{ $petani->count() }}"
subtitle="{{ $petani->where('jenis_kelamin', 1)->count() }} Laki-laki, {{ $petani->where('jenis_kelamin', 0)->count() }} Perempuan"
count="{{ number_format($totalPetani ?? 0, 0, ',', '.') }}"
subtitle="{{ number_format($totalPetaniLakiLaki ?? 0, 0, ',', '.') }} Laki-laki, {{ number_format($totalPetaniPerempuan ?? 0, 0, ',', '.') }} Perempuan"
icon="user-check"
color="primary"
/>
</div>
<div class="w-25">
<div class="col-12 col-md-4">
<x-summary-card
title="Lahan & Tanaman"
count="{{ number_format(($totalLuasLahan ?? 0) / 10000, 2, ',', '.') }} ha"
subtitle="{{ $totalTanaman ?? 0 }} tanaman dari {{ $komoditas?->count() }} komoditas utama"
count="{{ number_format($totalLuasLahan ?? 0, 2, ',', '.') }} ha"
subtitle="{{ number_format($totalLahan ?? 0, 0, ',', '.') }} lahan, {{ number_format($totalTanaman ?? 0, 0, ',', '.') }} tanaman"
icon="trees"
color="success"
/>
</div>
<div class="w-25">
<div class="col-12 col-md-4">
<x-summary-card
title="Segmentasi"
count="{{ $skor->where('segmen', 'Penggerak')->count() }} Penggerak"
subtitle="{{ $skor->where('segmen', 'Perintis')->count() }} Perintis, {{ $skor->where('segmen', 'Pemerhati')->count() }} Pemerhati, {{ $skor->where('segmen', 'Pemula')->count() }} Pemula"
count="{{ number_format((int) ($skor->firstWhere('segmen', 'Penggerak')->total ?? 0), 0, ',', '.') }} Penggerak"
subtitle="{{ number_format((int) ($skor->firstWhere('segmen', 'Perintis')->total ?? 0), 0, ',', '.') }} Perintis, {{ number_format((int) ($skor->firstWhere('segmen', 'Pemerhati')->total ?? 0), 0, ',', '.') }} Pemerhati, {{ number_format((int) ($skor->firstWhere('segmen', 'Pemula')->total ?? 0), 0, ',', '.') }} Pemula"
icon="star"
color="info"
/>
</div>
</div>
<div class="card">
<div class="row g-6 indicator-filter-row">
<div class="col-12">
<div class="card card-border-shadow-primary">
<div class="card-body py-3">
<form method="GET" action="{{ route('petani.index') }}" id="petani-filter-form">
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
<div class="indicator-filter-bar" id="indicator-filter-bar" aria-label="Filter data petani">
<div class="indicator-filter-line">
<div class="indicator-filter-section-label">
<i class="icon-base ti tabler-map-2 me-2"></i>
<span>Wilayah</span>
</div>
<div class="filter-item">
<input
type="hidden"
name="kabupaten_kota_id"
value="{{ $filters['kabupaten_kota_id'] }}"
>
<div
id="filter-kabupaten"
class="form-control form-control-sm indicator-filter-static"
data-filter-key="kabupaten"
data-filter-value="{{ $filters['kabupaten_kota_id'] }}"
aria-label="Filter Kabupaten"
role="text"
>
{{ preg_replace('/^(kabupaten|kota)\s+/i', '', $selectedKabupaten?->nama ?? 'Ngada') }}
</div>
</div>
<div class="filter-item">
<select
name="kecamatan_id"
id="filter-kecamatan"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="kecamatan"
aria-label="Filter Kecamatan"
@disabled(empty($filters['kabupaten_kota_id']))
>
<option value="">Semua Kecamatan</option>
@foreach($kecamatanOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['kecamatan_id'] === (string) $option->id)>
{{ $option->nama }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select
name="desa_kelurahan_id"
id="filter-desa"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="desa"
aria-label="Filter Desa"
@disabled(empty($filters['kecamatan_id']))
>
<option value="">Semua Desa</option>
@foreach($desaOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['desa_kelurahan_id'] === (string) $option->id)>
{{ $option->nama }}
</option>
@endforeach
</select>
</div>
</div>
<hr class="my-3">
<div class="indicator-filter-line">
<div class="indicator-filter-section-label">
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
<span>Data</span>
</div>
<div class="filter-item">
<select
name="komoditas_id"
id="filter-komoditas"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="komoditas"
aria-label="Filter Komoditas"
>
<option value="">Semua Komoditas</option>
@foreach($komoditasOptions as $option)
<option value="{{ $option->id }}" @selected((string) $filters['komoditas_id'] === (string) $option->id)>
{{ $option->nama }}
</option>
@endforeach
</select>
</div>
<div class="filter-item">
<select
name="sort_by"
id="filter-sort-by"
class="form-select form-select-sm indicator-filter-select"
data-filter-key="sort_by"
aria-label="Urutkan data petani"
>
<option value="">Urut Terbaru</option>
<option value="nama_asc" @selected($filters['sort_by'] === 'nama_asc')>Nama A-Z</option>
<option value="nama_desc" @selected($filters['sort_by'] === 'nama_desc')>Nama Z-A</option>
<option value="luas_lahan_desc" @selected($filters['sort_by'] === 'luas_lahan_desc')>Luas Lahan Terbesar</option>
<option value="luas_lahan_asc" @selected($filters['sort_by'] === 'luas_lahan_asc')>Luas Lahan Terkecil</option>
</select>
</div>
<div class="filter-item">
<div class="input-group input-group-sm">
<input
type="text"
name="search"
class="form-control"
placeholder="Cari nama / KTP..."
value="{{ $filters['search'] }}"
aria-label="Cari nama atau KTP"
>
<button type="submit" class="btn btn-outline-secondary" aria-label="Cari data petani">
<i class="icon-base ti tabler-search"></i>
</button>
</div>
</div>
<button
type="submit"
class="btn btn-outline-secondary btn-sm"
aria-label="Terapkan filter data petani"
>
<i class="icon-base ti tabler-filter me-1"></i>
Filter
</button>
<a
href="{{ route('petani.index') }}"
class="btn btn-outline-secondary btn-sm"
aria-label="Reset filter data petani"
>
<i class="icon-base ti tabler-refresh me-1"></i>
Reset
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="card" id="data-petani">
<div class="card-header border-bottom">
<h5 class="card-title mb-0">Data Petani</h5>
</div>
<div class="card-body border-bottom pt-4">
<form method="GET" action="{{ route('petani.index') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0">
<form method="GET" action="{{ route('petani.index') }}" class="row g-3 align-items-end">
<input type="hidden" name="komoditas_id" value="{{ $filters['komoditas_id'] }}">
<input type="hidden" name="kabupaten_kota_id" value="{{ $filters['kabupaten_kota_id'] }}">
<input type="hidden" name="kecamatan_id" value="{{ $filters['kecamatan_id'] }}">
<input type="hidden" name="desa_kelurahan_id" value="{{ $filters['desa_kelurahan_id'] }}">
<input type="hidden" name="search" value="{{ $filters['search'] }}">
<input type="hidden" name="sort_by" value="{{ $filters['sort_by'] }}">
<div class="col-md-auto">
<div class="d-flex align-items-center">
<label class="me-2 text-nowrap">Show</label>
@@ -49,11 +297,7 @@
<label class="ms-2 text-nowrap">entries</label>
</div>
</div>
<div class="col-md-auto d-flex gap-2">
<div class="input-group input-group-sm w-auto">
<input type="text" name="search" class="form-control" placeholder="Cari Nama..." value="{{ request('search') }}">
<button type="submit" class="btn btn-outline-primary"><i class="ti tabler-search"></i></button>
</div>
<div class="col-md-auto ms-md-auto">
<a href="{{ route('petani.create') }}" class="btn btn-sm btn-primary">
<i class="ti tabler-plus me-1"></i> Tambah
</a>
@@ -67,23 +311,39 @@
<tr>
<th>No</th>
<th>Nama</th>
<th>Jenis Kelamin</th>
<th>Usia</th>
<th>Desa/Kelurahan</th>
<th>Komoditas Utama</th>
<th>Luas Lahan</th>
@if($showFasilitatorColumn)
<th>Fasilitator</th>
@endif
<th>Segmentasi</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
@forelse($petani as $index => $item)
<tr>
<tr class="petani-row">
<td>{{ $loop->iteration }}</td>
<td><span class="text-heading">{{ $item->nama }}</span></td>
<td><span class="text-muted">{{ $item->jenis_kelamin ? 'Laki-laki' : 'Perempuan' }}</span></td>
<td><span class="text-muted">{{ $item->umur }} tahun</span></td>
<td>
<a href="{{ route('petani.edit', $item->id) }}" class="text-heading d-block">{{ $item->nama }}</a>
<small class="text-muted">
{{ $item->jenis_kelamin ? 'Laki-laki' : 'Perempuan' }}
@if(! is_null($item->umur))
, {{ $item->umur }} tahun
@endif
</small>
</td>
<td><span class="text-muted">{{ $item->desaKelurahan?->nama }}, {{ $item->desaKelurahan?->kecamatan?->nama }}, {{ $item->desaKelurahan?->kecamatan?->kabupatenKota?->nama }}, {{ $item->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi?->nama }}</span></td>
<td>{{ $item->komoditas?->nama }}</td>
<td>
<span class="text-muted">
{{ number_format((float) ($item->total_luas_lahan ?? 0), 2, ',', '.') }} ha
</span>
</td>
@if($showFasilitatorColumn)
<td><span class="text-muted">{{ $item->creator?->name ?? '-' }}</span></td>
@endif
<td>
@if($item->segmen() == 'Penggerak')
<span class="badge bg-label-success">Penggerak</span>
@@ -104,7 +364,7 @@
</td>
</tr>
@empty
<tr><td colspan="6" class="text-center py-4">Tidak ada data ditemukan.</td></tr>
<tr><td colspan="{{ $showFasilitatorColumn ? 9 : 8 }}" class="text-center py-4">Tidak ada data ditemukan.</td></tr>
@endforelse
</tbody>
</table>
@@ -122,6 +382,63 @@
const csrfToken = "{{ csrf_token() }}";
$(document).ready(function () {
const kecamatanUrlTemplate = '{{ route("kecamatan.bykabupatenkota", ":id") }}';
const desaUrlTemplate = '{{ route("desakelurahan.bykecamatan", ":id") }}';
const $kabupatenFilter = $('#filter-kabupaten');
const $kecamatanFilter = $('#filter-kecamatan');
const $desaFilter = $('#filter-desa');
function resetSelect($select, placeholder, disabled = true) {
$select.empty().append(`<option value="">${placeholder}</option>`);
$select.prop('disabled', disabled);
}
function fillSelect($select, data, selectedValue, placeholder) {
resetSelect($select, placeholder, false);
data.forEach(function (item) {
const option = new Option(item.nama, item.id, false, String(selectedValue || '') === String(item.id));
$select.append(option);
});
}
$kabupatenFilter.on('change', function () {
const kabupatenId = $(this).val();
resetSelect($kecamatanFilter, 'Semua Kecamatan');
resetSelect($desaFilter, 'Semua Desa');
if (!kabupatenId) {
return;
}
$.get(kecamatanUrlTemplate.replace(':id', kabupatenId), function (data) {
fillSelect($kecamatanFilter, data, '', 'Semua Kecamatan');
});
});
$kecamatanFilter.on('change', function () {
const kecamatanId = $(this).val();
resetSelect($desaFilter, 'Semua Desa');
if (!kecamatanId) {
return;
}
$.get(desaUrlTemplate.replace(':id', kecamatanId), function (data) {
fillSelect($desaFilter, data, '', 'Semua Desa');
});
});
$(document).on('click', '.petani-row', function (event) {
if ($(event.target).closest('a, button, input, select, textarea, label').length) {
return;
}
window.location.href = this.querySelector('.edit-record').href;
});
$(document).on('click', '.delete-record', function () {
const id = $(this).data('id');
const name = $(this).data('nama');
@@ -517,7 +517,7 @@
<div class="row mt-3">
<div class="col-12 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Lahan</button>
</div>
</div>
@@ -406,8 +406,8 @@
<div class="row mt-3">
<div class="col-12 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<button id="delete-lahan"type="button" class="btn btn-danger data-submit wave-effect wave-light"><i class="ti tabler-trash me-2"></i> Hapus Lahan</button>
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2 wave-effect wave-light">Kembali</a>
<button type="submit" class="btn btn-success data-submit wave-effect wave-light"><i class="ti tabler-device-floppy me-2"></i> Simpan Lahan</button>
</div>
</div>
@@ -86,6 +86,7 @@
</div>
<div class="mb-3">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
</div>
</form>
@@ -77,6 +77,7 @@
</table>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -167,10 +167,13 @@
Tambah Pelatihan
</button>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Riwayat Pelatihan
</button>
<div class="d-flex flex-wrap justify-content-end gap-2">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Riwayat Pelatihan
</button>
</div>
</div>
</form>
</div>
@@ -347,10 +350,13 @@
Tambah Sertifikasi
</button>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Sertifikasi Keahlian
</button>
<div class="d-flex flex-wrap justify-content-end gap-2">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan Sertifikasi Keahlian
</button>
</div>
</div>
</form>
</div>
@@ -499,6 +499,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
Simpan
</button>
@@ -530,6 +530,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1039,6 +1040,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1352,6 +1354,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1542,6 +1545,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1678,9 +1682,12 @@
Tambah Komoditas
</button>
<button type="submit" class="btn btn-success">
Simpan Produktivitas
</button>
<div class="d-flex flex-wrap justify-content-end gap-2">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
<button type="submit" class="btn btn-success">
Simpan
</button>
</div>
</div>
</form>
</div>
@@ -1725,6 +1732,7 @@
</div>
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -399,6 +399,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -567,6 +568,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -696,6 +698,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -915,6 +918,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1043,6 +1047,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -398,6 +398,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -566,6 +567,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -694,6 +696,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -912,6 +915,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -1039,6 +1043,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary me-2">Kembali</a>
<button type="submit" class="btn btn-success">
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
Simpan
@@ -85,7 +85,7 @@
</div>
<div class="mt-4 text-end">
<a href="{{ route('petani.index') }}" class="btn btn-label-secondary me-2">Kembali</a>
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-label-secondary me-2">Kembali</a>
</div>
</form>
</div>
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -21,7 +21,7 @@ Route::get('/', function() {
return redirect()->route('dashboard.survey-petani');
}
return redirect()->route('dashboard.produktivitas');
return redirect()->route('dashboard.karakteristik-petani');
})->name('home');
Route::middleware('auth')
@@ -41,11 +41,14 @@ Route::middleware('auth')
// Home
Route::get('/dashboard/produktivitas/map-information', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'mapInformation'])->name('dashboard.produktivitas.map-information');
Route::get('/dashboard/produktivitas', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'produktivitas'])->name('dashboard.produktivitas');
Route::get('/dashboard/pasar', [App\Http\Controllers\Web\Dashboard\SummaryController::class, 'pasar'])->name('dashboard.pasar');
// Dashboard
Route::get('/dashboard/petani', [App\Http\Controllers\Web\Dashboard\ProfilPetaniController::class, 'index'])->name('dashboard.petani');
Route::get('/dashboard/bumdes', [App\Http\Controllers\Web\Bumdes\PengurusBumdesController::class, 'index'])->name('dashboard.bumdes');
Route::get('/dashboard/petani/karakteristik-petani', [App\Http\Controllers\Web\Dashboard\DemografiPetaniController::class, 'karakteristik'])->name('dashboard.karakteristik-petani');
Route::get('/dashboard/petani/demografi', [App\Http\Controllers\Web\Dashboard\DemografiPetaniController::class, 'index'])->name('dashboard.demografi-petani');
Route::get('/dashboard/petani/demografi/table',[App\Http\Controllers\Web\Dashboard\DemografiPetaniController::class, 'data'])->name('dashboard.demografi-petani.data');
Route::get('/dashboard/petani/indikator-pertanian/data', [App\Http\Controllers\Web\Dashboard\IndikatorPertanianController::class, 'data'])->name('dashboard.indikator-pertanian.data');
@@ -0,0 +1,129 @@
<?php
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->softDeletes();
});
Schema::create('master_kabupaten_kota', function (Blueprint $table) {
$table->id();
$table->integer('provinsi_id');
});
Schema::create('master_kecamatan', function (Blueprint $table) {
$table->id();
$table->integer('kabupaten_kota_id');
});
Schema::create('master_desa_kelurahan', function (Blueprint $table) {
$table->id();
$table->integer('kecamatan_id');
});
Schema::create('petani', function (Blueprint $table) {
$table->id();
$table->integer('komoditas_id');
$table->integer('desa_kelurahan_id');
$table->softDeletes();
});
Schema::create('map_lahan', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->decimal('luas_lahan', 12, 4);
$table->softDeletes();
});
Schema::create('survey_panen', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->decimal('hasil_komoditas_utama_permusim', 12, 2)->nullable();
$table->decimal('hasil_komoditas_lainnya_permusim', 12, 2)->nullable();
$table->softDeletes();
});
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'],
['id' => 2, 'nama' => 'Vanili'],
['id' => 3, 'nama' => 'Kemiri'],
['id' => 4, 'nama' => 'Kopi'],
]);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'kecamatan_id' => 1],
['id' => 2, 'kecamatan_id' => 1],
]);
DB::table('petani')->insert([
['id' => 1, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 2, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 3, 'komoditas_id' => 4, 'desa_kelurahan_id' => 2, 'deleted_at' => null],
['id' => 4, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => '2026-01-01'],
['id' => 5, 'komoditas_id' => 2, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
]);
DB::table('map_lahan')->insert([
['petani_id' => 1, 'luas_lahan' => 0.5, 'deleted_at' => null],
['petani_id' => 1, 'luas_lahan' => 1.5, 'deleted_at' => null],
['petani_id' => 2, 'luas_lahan' => 3, 'deleted_at' => null],
['petani_id' => 3, 'luas_lahan' => 2, 'deleted_at' => null],
['petani_id' => 4, 'luas_lahan' => 50, 'deleted_at' => null],
['petani_id' => 1, 'luas_lahan' => 100, 'deleted_at' => '2026-01-01'],
['petani_id' => 5, 'luas_lahan' => 0, 'deleted_at' => null],
]);
DB::table('survey_panen')->insert([
['petani_id' => 1, 'hasil_komoditas_utama_permusim' => 300, 'deleted_at' => null],
['petani_id' => 2, 'hasil_komoditas_utama_permusim' => 600, 'deleted_at' => null],
['petani_id' => 3, 'hasil_komoditas_utama_permusim' => 400, 'deleted_at' => null],
['petani_id' => 4, 'hasil_komoditas_utama_permusim' => 10000, 'deleted_at' => null],
['petani_id' => 1, 'hasil_komoditas_utama_permusim' => 20000, 'deleted_at' => '2026-01-01'],
['petani_id' => 5, 'hasil_komoditas_utama_permusim' => 10, 'deleted_at' => null],
]);
DB::table('survey_panen')->where('petani_id', 1)->update(['hasil_komoditas_lainnya_permusim' => 500]);
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'actualProductivityByCommodityChart');
$this->chart = fn (array $filters = []) => $method->invoke($controller, new Request($filters));
});
test('actual productivity uses seasonal survey harvest and counts each plot once', function () {
$chart = ($this->chart)();
expect($chart['labels'])->toBe(['Kakao', 'Vanili', 'Kemiri', 'Kopi']);
expect($chart['series'])->toEqual([180, 0, 0, 200]);
});
test('actual productivity respects region and commodity filters', function () {
expect(($this->chart)(['desaKelurahan_id' => 1])['series'])->toEqual([180, 0, 0, 0]);
expect(($this->chart)(['desaKelurahan_id' => 2, 'komoditas_id' => 4])['series'])->toEqual([0, 0, 0, 200]);
expect(($this->chart)(['desaKelurahan_id' => 1, 'komoditas_id' => 4])['series'])->toEqual([0, 0, 0, 0]);
expect(($this->chart)(['provinsi_id' => 99])['series'])->toEqual([0, 0, 0, 0]);
});
test('actual productivity rounds kg per hectare to two decimals', function () {
DB::table('map_lahan')->where('petani_id', 5)->update(['luas_lahan' => 0.3]);
expect(($this->chart)()['series'][1])->toBe(33.33);
});
test('productivity cards display the same values and commodities as the chart', function () {
DB::table('map_lahan')->where('petani_id', 5)->update(['luas_lahan' => 0.3]);
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'productivityKpiCards');
foreach ([[], ['desaKelurahan_id' => 2], ['komoditas_id' => 1]] as $filters) {
$chart = ($this->chart)($filters);
$cards = $method->invoke($controller, $chart);
expect(array_column($cards, 'label'))->toBe(array_map(
fn ($name) => 'Produktivitas Aktual '.$name,
$chart['labels'],
));
expect(array_column($cards, 'value'))->toBe(array_map(
fn ($value) => number_format($value, 2, ',', '.'),
$chart['series'],
));
expect(array_column($cards, 'unit'))->toBe(array_fill(0, count($chart['labels']), 'kg/ha'));
}
});
@@ -0,0 +1,117 @@
<?php
use App\Enums\AksesPasar\PraktikFermentasi;
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
});
foreach ([
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->integer($parent);
});
}
Schema::create('petani', function (Blueprint $table) {
$table->id();
$table->integer('komoditas_id')->nullable();
$table->integer('desa_kelurahan_id');
$table->softDeletes();
});
Schema::create('akses_pasar', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->string('praktik_fermentasi')->nullable();
$table->softDeletes();
});
Schema::create('petani_skor', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->float('skor_kesiapan_adopsi');
$table->softDeletes();
});
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Kopi'], ['id' => 3, 'nama' => 'Vanili'],
]);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
]);
foreach ([
[1, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
[2, 1, 1, PraktikFermentasi::RUTIN_KELOMPOK->value],
[3, 2, 2, PraktikFermentasi::PERNAH_SENDIRI->value],
[4, 2, 2, PraktikFermentasi::PERNAH_KELOMPOK->value],
[5, 3, 2, PraktikFermentasi::BELUM_PERNAH->value],
[6, 3, 2, null],
[7, 1, 1, ''],
[8, 2, 2, PraktikFermentasi::RUTIN_SENDIRI->value],
[9, null, 2, PraktikFermentasi::BELUM_PERNAH->value],
[10, 1, 1, PraktikFermentasi::RUTIN_SENDIRI->value],
[11, 1, 1, 'Tidak dikenal'],
] as [$id, $commodity, $village, $practice]) {
DB::table('petani')->insert([
'id' => $id, 'komoditas_id' => $commodity, 'desa_kelurahan_id' => $village,
'deleted_at' => $id === 10 ? '2026-01-01' : null,
]);
DB::table('akses_pasar')->insert([
'petani_id' => $id, 'praktik_fermentasi' => $practice,
'deleted_at' => $id === 8 ? '2026-01-01' : null,
]);
}
DB::table('petani_skor')->insert([
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.1, 'deleted_at' => null],
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.6, 'deleted_at' => null],
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.4, 'deleted_at' => '2026-01-01'],
]);
$controller = app(SummaryController::class);
$query = new ReflectionMethod($controller, 'filteredAksesPasar');
$chart = new ReflectionMethod($controller, 'fermentationPracticeByCommodityChart');
$this->fermentationChart = fn (array $filters = []) => $chart->invoke(
$controller, $query->invoke($controller, new Request($filters)),
);
});
test('fermentation groups individual and collective responses by commodity', function () {
expect(($this->fermentationChart)())->toBe([
'labels' => ['Belum ditentukan', 'Kakao', 'Kopi', 'Vanili'],
'series' => [
['name' => 'Ya, rutin', 'data' => [0, 2, 0, 0]],
['name' => 'Pernah mencoba', 'data' => [0, 0, 2, 0]],
['name' => 'Belum pernah', 'data' => [1, 0, 0, 1]],
],
]);
});
test('fermentation respects region and commodity filters', function () {
$chart = ($this->fermentationChart)(['desaKelurahan_id' => 2, 'komoditas_id' => 2]);
expect($chart['labels'])->toBe(['Kopi']);
expect(array_column($chart['series'], 'data'))->toBe([[0], [2], [0]]);
expect(($this->fermentationChart)(['desaKelurahan_id' => 1])['labels'])->toBe(['Kakao']);
});
test('fermentation respects segment filters and excludes deleted scores', function () {
$chart = ($this->fermentationChart)(['segmen' => 'Pemerhati']);
expect($chart['labels'])->toBe(['Kakao']);
expect(array_column($chart['series'], 'data'))->toBe([[1], [0], [0]]);
expect(($this->fermentationChart)(['segmen' => 'Penggerak'])['labels'])->toBe(['Kakao']);
});
test('fermentation returns empty grouped series when no matching responses exist', function () {
$chart = ($this->fermentationChart)(['provinsi_id' => 99]);
expect($chart['labels'])->toBe([]);
expect(array_column($chart['series'], 'data'))->toBe([[], [], []]);
});
+338
View File
@@ -0,0 +1,338 @@
<?php
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->softDeletes();
});
foreach ([
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->integer($parent);
});
}
Schema::create('petani', function (Blueprint $table) {
$table->id();
$table->integer('komoditas_id');
$table->integer('desa_kelurahan_id');
$table->softDeletes();
});
Schema::create('harga_jual_panen', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->string('bentuk_produk');
$table->integer('harga_panen_terakhir')->nullable();
$table->integer('harga_tahun_lalu')->nullable();
$table->integer('harga_dua_tahun_lalu')->nullable();
});
Schema::create('petani_skor', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->float('skor_kesiapan_adopsi');
$table->softDeletes();
});
Schema::create('akses_pasar', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->text('tujuan_penjualan')->nullable();
$table->text('sifat_hubungan_bisnis')->nullable();
$table->string('frekuensi_penjualan')->nullable();
$table->integer('skor_pengalaman')->nullable();
$table->integer('skor_kesadaran_premium')->nullable();
$table->softDeletes();
});
DB::table('master_komoditas')->insert([
['id' => 1, 'nama' => 'Kakao'], ['id' => 2, 'nama' => 'Vanili'],
['id' => 3, 'nama' => 'Kopi'], ['id' => 4, 'nama' => 'Kemiri'],
]);
DB::table('master_kabupaten_kota')->insert(['id' => 1, 'provinsi_id' => 1]);
DB::table('master_kecamatan')->insert(['id' => 1, 'kabupaten_kota_id' => 1]);
DB::table('master_desa_kelurahan')->insert([
['id' => 1, 'kecamatan_id' => 1], ['id' => 2, 'kecamatan_id' => 1],
]);
DB::table('petani')->insert([
['id' => 1, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 2, 'komoditas_id' => 1, 'desa_kelurahan_id' => 2, 'deleted_at' => null],
['id' => 3, 'komoditas_id' => 2, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 4, 'komoditas_id' => 3, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 5, 'komoditas_id' => 4, 'desa_kelurahan_id' => 1, 'deleted_at' => null],
['id' => 6, 'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'deleted_at' => '2026-01-01'],
]);
foreach ([
[1, 'Biji kering/Biji Kupas', 10000], [2, 'Biji kering/Biji Kupas', 20000],
[1, 'Biji kering/Biji Kupas', 0], [1, 'Biji kering/Biji Kupas', null],
[1, 'Biji kering/Biji Kupas', -1000], [1, 'Biji basah', 90000],
[1, 'Buah segar hasil petik', 5000],
[3, 'Buah segar hasil petik', 30000], [3, 'Biji kering/Biji Kupas', 500000],
[3, 'Terfermentasi (Rp/kg)', 900000], [4, 'Buah segar hasil petik', 12000],
[5, 'Buah segar hasil petik', 8000], [5, 'Biji kering/Biji Kupas', 40000],
[6, 'Biji kering/Biji Kupas', 1000000],
] as [$farmer, $product, $price]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product, 'harga_panen_terakhir' => $price,
]);
}
DB::table('petani_skor')->insert([
['petani_id' => 1, 'skor_kesiapan_adopsi' => 4.2, 'deleted_at' => null],
['petani_id' => 2, 'skor_kesiapan_adopsi' => 2.5, 'deleted_at' => null],
['petani_id' => 3, 'skor_kesiapan_adopsi' => 4.5, 'deleted_at' => '2026-01-01'],
]);
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'marketDashboardData');
$this->marketCards = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceCards'];
$this->marketCharts = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['marketCharts'];
$this->priceTrend = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['priceTrend'];
$this->premiumAwareness = fn (array $filters = []) => $method->invoke($controller, new Request($filters))['premiumAwareness'];
});
test('market shows all four prices using the appropriate product and positive active farmer prices', function () {
$cards = ($this->marketCards)();
expect(array_column($cards, 'label'))->toBe([
'Kakao - Harga Biji Kering', 'Vanili - Harga Buah Segar',
'Kopi - Harga Ceri Merah', 'Kemiri - Harga Gelondong',
]);
expect(array_column($cards, 'value'))->toBe(['Rp 15.000', 'Rp 30.000', 'Rp 12.000', 'Rp 8.000']);
expect(array_column($cards, 'records'))->toBe([2, 1, 1, 1]);
});
test('market prices respect each region level and commodity selection', function () {
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
expect(($this->marketCards)([$region => 1])[0]['value'])
->toBe($region === 'desaKelurahan_id' ? 'Rp 10.000' : 'Rp 15.000');
expect(array_column(($this->marketCards)([$region => 99]), 'records'))->toBe([0, 0, 0, 0]);
}
$cards = ($this->marketCards)(['komoditas_id' => 2]);
expect(array_column($cards, 'records'))->toBe([0, 1, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 30.000', 'Rp 0', 'Rp 0']);
});
test('market prices respect segment selection and ignore deleted scores', function () {
$cards = ($this->marketCards)(['segmen' => 'Penggerak']);
expect(array_column($cards, 'records'))->toBe([1, 0, 0, 0]);
expect($cards[0]['value'])->toBe('Rp 10.000');
expect(($this->marketCards)(['segmen' => 'Pemerhati'])[0]['value'])->toBe('Rp 20.000');
});
test('market keeps four empty cards when no prices or active commodities are available', function () {
DB::table('master_komoditas')->update(['deleted_at' => '2026-01-01']);
$cards = ($this->marketCards)();
expect(array_column($cards, 'records'))->toBe([0, 0, 0, 0]);
expect(array_column($cards, 'value'))->toBe(['Rp 0', 'Rp 0', 'Rp 0', 'Rp 0']);
});
test('market charts count multiple channels once per respondent and use answered question denominators', function () {
DB::table('akses_pasar')->insert([
[
'petani_id' => 1,
'tujuan_penjualan' => json_encode(['Koperasi', 'BUMDes', 'Koperasi', 'Lainnya']),
'sifat_hubungan_bisnis' => 'Bebas, bisa pindah pembeli kapan saja',
'frekuensi_penjualan' => 'Langsung setelah panen',
'skor_pengalaman' => 5,
],
[
'petani_id' => 2,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Titip jual,dibayar setelah produk terjual',
'frekuensi_penjualan' => null,
'skor_pengalaman' => 1,
],
[
'petani_id' => 3,
'tujuan_penjualan' => '[]',
'sifat_hubungan_bisnis' => '',
'frekuensi_penjualan' => 'Saat butuh uang mendesak',
'skor_pengalaman' => 3,
],
]);
$charts = ($this->marketCharts)();
expect($charts['saluranPenjualan']['series'])->toBe([0, 2, 1, 0, 1]);
expect($charts['saluranPenjualan']['respondents'])->toBe(2);
expect($charts['saluranPenjualan']['percentages'])->toBe([0.0, 100.0, 50.0, 0.0, 50.0]);
expect($charts['hubunganPembeli']['series'])->toBe([1, 0, 0, 1, 0, 0]);
expect($charts['hubunganPembeli']['respondents'])->toBe(2);
expect($charts['waktuPenjualan']['series'])->toBe([1, 0, 1]);
expect($charts['waktuPenjualan']['respondents'])->toBe(2);
expect($charts['aksesSaluranLain']['series'])->toBe([1, 0, 1, 0, 1]);
expect($charts['aksesSaluranLain']['percentages'])->toBe([33.3, 0.0, 33.3, 0.0, 33.3]);
});
test('market charts respect all dashboard filters and exclude deleted farmers and responses', function () {
foreach ([1, 2, 3, 6] as $farmer) {
DB::table('akses_pasar')->insert([
'petani_id' => $farmer,
'tujuan_penjualan' => json_encode(['Koperasi']),
'sifat_hubungan_bisnis' => 'Langganan tetap, tapi tidak terikat',
'frekuensi_penjualan' => 'Ditumpuk dulu beberapa waktu',
'skor_pengalaman' => 4,
]);
}
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'tujuan_penjualan' => json_encode(['BUMDes']), 'deleted_at' => '2026-01-01',
]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
foreach (($this->marketCharts)([$region => 1]) as $chart) {
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 2 : 3);
}
foreach (($this->marketCharts)([$region => 99]) as $chart) {
expect($chart['respondents'])->toBe(0);
}
}
foreach ([['komoditas_id' => 1], ['komoditas_id' => 2], ['segmen' => 'Penggerak'], ['segmen' => 'Pemerhati']] as $filters) {
foreach (($this->marketCharts)($filters) as $chart) {
expect($chart['respondents'])->toBe($filters === ['komoditas_id' => 1] ? 2 : 1);
}
}
foreach (($this->marketCharts)(['komoditas_id' => 2, 'segmen' => 'Penggerak']) as $chart) {
expect($chart['respondents'])->toBe(0);
}
});
test('market charts keep ordered zero categories when answers are missing or invalid', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 1, 'tujuan_penjualan' => '[null, {}, "Unknown"]',
'sifat_hubungan_bisnis' => 'Unknown', 'frekuensi_penjualan' => '', 'skor_pengalaman' => 9,
]);
$charts = ($this->marketCharts)();
expect(array_map(fn ($chart) => count($chart['series']), $charts))->toBe([
'saluranPenjualan' => 5, 'hubunganPembeli' => 6, 'waktuPenjualan' => 3, 'aksesSaluranLain' => 5,
]);
foreach ($charts as $chart) {
expect($chart['respondents'])->toBe(0);
expect(array_sum($chart['series']))->toBe(0);
expect(array_sum($chart['percentages']))->toBe(0);
}
});
test('price trend averages each product and commodity chronologically without treating missing prices as zero', function () {
DB::table('harga_jual_panen')->delete();
foreach ([
[1, 'Buah segar hasil petik', 10000, 8000, 6000],
[2, 'Buah segar hasil petik', 20000, 12000, null],
[3, 'Buah segar hasil petik', 30000, 20000, 10000],
[1, 'Biji basah', 0, 7000, 4000],
[1, 'Biji kering/Biji Kupas', -1000, null, 0],
[1, 'Terfermentasi (Rp/kg)', null, 20000, 10000],
[6, 'Buah segar hasil petik', 1000000, 1000000, 1000000],
] as [$farmer, $product, $latest, $lastYear, $twoYearsAgo]) {
DB::table('harga_jual_panen')->insert([
'petani_id' => $farmer, 'bentuk_produk' => $product,
'harga_panen_terakhir' => $latest, 'harga_tahun_lalu' => $lastYear, 'harga_dua_tahun_lalu' => $twoYearsAgo,
]);
}
$trend = ($this->priceTrend)();
$commodities = array_column($trend['commodities'], null, 'id');
expect($trend['periods'])->toBe(['Dua Tahun Lalu', 'Tahun Lalu', 'Panen Terakhir']);
expect(array_column($commodities[1]['series'], 'name'))->toBe([
'Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi',
]);
expect(array_column($commodities[1]['series'], 'data'))->toBe([
[6000.0, 10000.0, 15000.0], [4000.0, 7000.0, null], [null, null, null], [10000.0, 20000.0, null],
]);
expect($commodities[2]['series'][0]['data'])->toBe([10000.0, 20000.0, 30000.0]);
});
test('price trend respects dashboard region commodity and segment filters', function () {
// Historical prices must follow the same filters as the latest harvest.
DB::table('harga_jual_panen')->where('harga_panen_terakhir', '>', 0)
->update(['harga_tahun_lalu' => DB::raw('harga_panen_terakhir / 2')]);
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$commodities = array_column(($this->priceTrend)([$region => 1])['commodities'], null, 'id');
$price = $region === 'desaKelurahan_id' ? 10000.0 : 15000.0;
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
foreach (($this->priceTrend)([$region => 99])['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
}
$trend = ($this->priceTrend)(['komoditas_id' => 2]);
expect(array_column($trend['commodities'], 'id'))->toBe([2]);
expect($trend['commodities'][0]['series'][0]['data'])->toBe([null, 15000.0, 30000.0]);
foreach (['Penggerak' => 10000.0, 'Pemerhati' => 20000.0] as $segment => $price) {
$commodities = array_column(($this->priceTrend)(['segmen' => $segment])['commodities'], null, 'id');
expect($commodities[1]['series'][2]['data'])->toBe([null, $price / 2, $price]);
expect($commodities[2]['series'][0]['data'])->toBe([null, null, null]);
}
});
test('price trend retains missing points and handles empty or deleted commodity selections', function () {
DB::table('harga_jual_panen')->delete();
$trend = ($this->priceTrend)();
expect($trend['commodities'])->toHaveCount(4);
foreach ($trend['commodities'] as $commodity) {
expect(array_column($commodity['series'], 'data'))->toBe(array_fill(0, 4, [null, null, null]));
}
expect(($this->priceTrend)(['komoditas_id' => 99])['commodities'])->toBe([]);
DB::table('master_komoditas')->where('id', 1)->update(['deleted_at' => '2026-01-01']);
expect(($this->priceTrend)(['komoditas_id' => 1])['commodities'])->toBe([]);
});
test('premium awareness counts each active farmer once and preserves the five ordered levels', function () {
DB::table('akses_pasar')->insert([
'petani_id' => 4, 'skor_kesadaran_premium' => 4, 'deleted_at' => '2026-01-01',
]);
foreach ([1 => 1, 2 => 3, 3 => 3, 4 => 5, 5 => null, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
DB::table('akses_pasar')->insert(['petani_id' => 1, 'skor_kesadaran_premium' => 2]);
$chart = ($this->premiumAwareness)();
expect($chart['series'])->toBe([1, 0, 2, 0, 1]);
expect($chart['percentages'])->toBe([25.0, 0.0, 50.0, 0.0, 25.0]);
expect($chart['respondents'])->toBe(4);
expect($chart['fullLabels'][0])->toBe('Belum pernah mendengar sama sekali');
expect($chart['fullLabels'][4])->toBe('Sudah mengakses dan mendapat harga premium secara konsisten');
});
test('premium awareness respects all dashboard region commodity and segment filters', function () {
foreach ([1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 5] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
foreach (['provinsi_id', 'kabupatenKota_id', 'kecamatan_id', 'desaKelurahan_id'] as $region) {
$chart = ($this->premiumAwareness)([$region => 1]);
expect($chart['respondents'])->toBe($region === 'desaKelurahan_id' ? 4 : 5);
expect(($this->premiumAwareness)([$region => 99])['respondents'])->toBe(0);
}
expect(($this->premiumAwareness)(['komoditas_id' => 1])['series'])->toBe([1, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2])['series'])->toBe([0, 0, 1, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Penggerak'])['series'])->toBe([1, 0, 0, 0, 0]);
expect(($this->premiumAwareness)(['segmen' => 'Pemerhati'])['series'])->toBe([0, 1, 0, 0, 0]);
expect(($this->premiumAwareness)(['komoditas_id' => 2, 'segmen' => 'Penggerak'])['respondents'])->toBe(0);
});
test('premium awareness retains empty categories and excludes missing or invalid scores', function () {
$emptyChart = ($this->premiumAwareness)();
foreach ([1 => 0, 2 => 6, 3 => null, 4 => -1, 5 => null] as $farmer => $score) {
DB::table('akses_pasar')->insert(['petani_id' => $farmer, 'skor_kesadaran_premium' => $score]);
}
$chart = ($this->premiumAwareness)();
expect($chart)->toBe($emptyChart);
expect($chart['series'])->toBe([0, 0, 0, 0, 0]);
expect($chart['respondents'])->toBe(0);
expect($chart['labels'])->toHaveCount(5);
expect(array_sum($chart['percentages']))->toBe(0);
});
+129
View File
@@ -0,0 +1,129 @@
<?php
use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama;
use App\Enums\PraktikBudidaya\PemberianPupuk;
use App\Http\Controllers\Web\Dashboard\SummaryController;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
beforeEach(function () {
foreach ([
'master_provinsi' => null,
'master_kabupaten_kota' => 'provinsi_id',
'master_kecamatan' => 'kabupaten_kota_id',
'master_desa_kelurahan' => 'kecamatan_id',
] as $name => $parent) {
Schema::create($name, function (Blueprint $table) use ($parent) {
$table->id();
$table->string('nama');
$table->text('path')->nullable();
if ($parent) {
$table->integer($parent);
}
$table->softDeletes();
});
DB::table($name)->insert(['id' => 1, 'nama' => 'Wilayah'] + ($parent ? [$parent => 1] : []));
}
Schema::create('master_komoditas', function (Blueprint $table) {
$table->id();
$table->string('nama');
});
foreach (['petani', 'profiles'] as $name) {
Schema::create($name, function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->softDeletes();
});
}
foreach (['map_lahan', 'map_tanaman'] as $name) {
Schema::create($name, function (Blueprint $table) use ($name) {
$table->id();
$table->integer('komoditas_id');
$table->integer('umur_tanaman')->nullable();
foreach (['serangan_hama', 'penyakit_tanaman', 'jenis_hama', 'jenis_penyakit'] as $column) {
$table->text($column)->nullable();
}
if ($name === 'map_lahan') {
$table->integer('petani_id')->nullable();
$table->integer('profile_id')->nullable();
$table->integer('desa_kelurahan_id');
$table->text('path');
$table->float('luas_lahan');
$table->integer('jumlah_tanaman');
} else {
$table->integer('lahan_id');
}
$table->timestamps();
$table->softDeletes();
});
}
Schema::create('praktik_budidaya', function (Blueprint $table) {
$table->id();
$table->integer('petani_id');
$table->integer('frekuensi_pemangkasan_utama');
$table->integer('pemberian_pupuk');
$table->text('kebiasaan_konservasi_tanah');
$table->timestamps();
$table->softDeletes();
});
DB::table('master_komoditas')->insert(['id' => 1, 'nama' => 'Kakao']);
DB::table('petani')->insert(['id' => 1, 'nama' => 'Petani A']);
DB::table('profiles')->insert(['id' => 1, 'nama' => 'Pemilik B']);
$land = [
'komoditas_id' => 1, 'desa_kelurahan_id' => 1, 'luas_lahan' => 1.5,
'jumlah_tanaman' => 50, 'umur_tanaman' => 8,
'path' => '[[[-8.1,121.1],[-8.2,121.1],[-8.1,121.2]]]',
'jenis_hama' => '["Penggerek buah"]',
];
DB::table('map_lahan')->insert($land + ['id' => 1, 'petani_id' => 1]);
DB::table('map_lahan')->insert($land + ['id' => 2, 'profile_id' => 1]);
DB::table('map_lahan')->insert($land + ['id' => 3, 'petani_id' => 1, 'deleted_at' => '2026-01-01']);
DB::table('map_tanaman')->insert([
'lahan_id' => 1, 'komoditas_id' => 1, 'umur_tanaman' => 4,
'jenis_penyakit' => '["Busuk buah"]',
]);
foreach ([
['2026-01-01', 1, null],
['2026-02-01', 4, null],
['2026-03-01', 5, '2026-03-02'],
] as [$updatedAt, $value, $deletedAt]) {
DB::table('praktik_budidaya')->insert([
'petani_id' => 1, 'frekuensi_pemangkasan_utama' => $value, 'pemberian_pupuk' => $value,
'kebiasaan_konservasi_tanah' => '["Menumpuk daun kering/sisa tanaman di sekitar pohon"]',
'updated_at' => $updatedAt, 'deleted_at' => $deletedAt,
]);
}
$controller = app(SummaryController::class);
$method = new ReflectionMethod($controller, 'productionMapLevelData');
$this->mapLevels = fn (array $filters = []) => $method->invoke($controller, new Request($filters));
});
test('land map combines plants and OPT with the latest active cultivation practice', function () {
$lands = collect(($this->mapLevels)()['lahan'])->keyBy('id');
expect($lands->keys()->all())->toBe([1, 2]);
expect($lands[1])->toMatchArray([
'nama_petani' => 'Petani A',
'luas' => 1.5,
'komoditas' => [['nama' => 'Kakao', 'jumlah' => 1, 'umur' => 4.0]],
'opt' => ['Busuk Buah', 'Penggerek Buah'],
'pemangkasan' => FrekuensiPemangkasanUtama::DUA_KALI_TAHUN->label(),
'pemupukan' => PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT->label(),
'praktik_baik' => ['Menumpuk daun kering/sisa tanaman di sekitar pohon'],
]);
expect($lands[2])->toMatchArray([
'nama_petani' => 'Pemilik B',
'komoditas' => [['nama' => 'Kakao', 'jumlah' => 50, 'umur' => 8.0]],
'pemangkasan' => 'Belum ada data',
'pemupukan' => 'Belum ada data',
'praktik_baik' => [],
]);
});
test('combined map respects region and commodity filters', function () {
expect(($this->mapLevels)(['desaKelurahan_id' => 1, 'komoditas_id' => 1])['lahan'])->toHaveCount(2);
expect(($this->mapLevels)(['desaKelurahan_id' => 99])['lahan'])->toBe([]);
expect(($this->mapLevels)(['komoditas_id' => 99])['lahan'])->toBe([]);
});