From 89c5a1371a3a395ff4971388404874bbbe548652 Mon Sep 17 00:00:00 2001 From: ahmadafriadi Date: Wed, 4 Nov 2026 16:20:33 +0700 Subject: [PATCH] Update Alert Profil Petani --- app/Enums/StatusDataPetani.php | 36 ++ .../Produktivitas/PetaniStatusController.php | 54 ++ app/Models/Pertanian/Petani.php | 16 + .../Produktivitas/KelengkapanPetani.php | 466 ++++++++++++++++++ ...4_100002_tambah_status_ke_petani_table.php | 27 + .../components/kelengkapan-petani.blade.php | 197 ++++++++ .../views/components/status-petani.blade.php | 102 ++++ resources/views/layout/main.blade.php | 35 ++ .../produktivitas/petani/header.blade.php | 10 +- routes/web.php | 1 + 10 files changed, 943 insertions(+), 1 deletion(-) create mode 100644 app/Enums/StatusDataPetani.php create mode 100644 app/Http/Controllers/Web/Produktivitas/PetaniStatusController.php create mode 100644 app/Services/Produktivitas/KelengkapanPetani.php create mode 100644 database/migrations/2026_11_04_100002_tambah_status_ke_petani_table.php create mode 100644 resources/views/components/kelengkapan-petani.blade.php create mode 100644 resources/views/components/status-petani.blade.php diff --git a/app/Enums/StatusDataPetani.php b/app/Enums/StatusDataPetani.php new file mode 100644 index 0000000..d200297 --- /dev/null +++ b/app/Enums/StatusDataPetani.php @@ -0,0 +1,36 @@ + 'Draf', + self::LENGKAP => 'Lengkap', + self::TERVERIFIKASI => 'Terverifikasi', + }; + } + + // Warna badge Vuexy (bg-label-{warna}) + public function warna(): string + { + return match ($this) { + self::DRAFT => 'secondary', + self::LENGKAP => 'info', + self::TERVERIFIKASI => 'success', + }; + } + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/app/Http/Controllers/Web/Produktivitas/PetaniStatusController.php b/app/Http/Controllers/Web/Produktivitas/PetaniStatusController.php new file mode 100644 index 0000000..c936be9 --- /dev/null +++ b/app/Http/Controllers/Web/Produktivitas/PetaniStatusController.php @@ -0,0 +1,54 @@ +bisaDitandaiOleh($request->user()), 403); + + $validated = $request->validate([ + 'status' => ['required', Rule::in([StatusDataPetani::LENGKAP->value, StatusDataPetani::DRAFT->value])], + ]); + $tujuan = StatusDataPetani::from($validated['status']); + + if ($petani->status === StatusDataPetani::TERVERIFIKASI) { + return back()->with('status_petani_error', 'Data petani sudah terverifikasi. Hubungi koordinator untuk mengubahnya.'); + } + + if ($tujuan === StatusDataPetani::LENGKAP) { + if ($petani->status !== StatusDataPetani::DRAFT) { + return back()->with('status_petani_error', 'Data petani sudah ditandai lengkap.'); + } + + $wajibKosong = KelengkapanPetani::untuk($petani)->jumlahWajibKosong(); + if ($wajibKosong > 0) { + return back()->with('status_petani_error', "Masih ada {$wajibKosong} isian wajib yang kosong."); + } + } + + if ($tujuan === StatusDataPetani::DRAFT && $petani->status !== StatusDataPetani::LENGKAP) { + return back()->with('status_petani_error', 'Data petani masih berstatus draf.'); + } + + $petani->status = $tujuan; + $petani->updated_by = $request->user()->id; + $petani->save(); + + return back()->with('status_petani_success', $tujuan === StatusDataPetani::LENGKAP + ? 'Data petani berhasil ditandai lengkap.' + : 'Tanda lengkap berhasil dibatalkan. Status kembali ke draf.'); + } +} diff --git a/app/Models/Pertanian/Petani.php b/app/Models/Pertanian/Petani.php index 4a66d9e..44aefde 100644 --- a/app/Models/Pertanian/Petani.php +++ b/app/Models/Pertanian/Petani.php @@ -4,6 +4,8 @@ namespace App\Models\Pertanian; use App\Enums\Pendidikan; use App\Enums\Pernikahan; +use App\Enums\Role; +use App\Enums\StatusDataPetani; use App\Models\User; use App\Models\Wilayah\DesaKelurahan; use App\Models\Pertanian\Lahan; @@ -70,6 +72,12 @@ class Petani extends Model 'pendidikan_terakhir' => Pendidikan::class, 'status_perkawinan' => Pernikahan::class, + 'status' => StatusDataPetani::class, + ]; + + // Sengaja tidak masuk $fillable: status hanya diubah lewat PetaniStatusController. + protected $attributes = [ + 'status' => 'draft', ]; public function getUmurAttribute(): ?int @@ -113,6 +121,14 @@ class Petani extends Model return $this->belongsTo(Komoditas::class, 'komoditas_id'); } + // Hanya enumerator yang menginput petani ini yang boleh menandai Lengkap / membatalkannya. + public function bisaDitandaiOleh(?User $user): bool + { + return $user + && $user->role === Role::ENUMERATOR + && (int) $this->created_by === (int) $user->id; + } + // Pertanyaan penyerbukan manual hanya berlaku untuk petani vanili. public function isKomoditasVanili(): bool { diff --git a/app/Services/Produktivitas/KelengkapanPetani.php b/app/Services/Produktivitas/KelengkapanPetani.php new file mode 100644 index 0000000..8e1f4b3 --- /dev/null +++ b/app/Services/Produktivitas/KelengkapanPetani.php @@ -0,0 +1,466 @@ + 'Profil', + 'lahan' => 'Lahan & Tanaman', + 'praktik-budidaya' => 'Praktik Budidaya', + 'sosial-ekonomi' => 'Sosial Ekonomi', + 'akses-pasar' => 'Akses Pasar', + ]; + + private const KONSERVASI_TIDAK_ADA = 'Tidak ada yang seperti itu di kebun saya'; + + /* + * Seluruh isian Profil Petani (total 209 untuk 1 lahan), per tabel. + * Aturan hitung: satu pertanyaan = satu isian; kolom repeater dihitung sekali per kolom; + * teks "Lainnya" tidak dihitung; provinsi/kabupaten/kecamatan ikut terisi bila desa terisi. + */ + private const ISIAN_PROFIL = [ // 15 + 3 wilayah = 18 + 'nama', 'nomor_telepon', 'alamat', 'kk', 'ktp', 'path_kk', 'path_ktp', 'foto', + 'jenis_kelamin', 'tempat_lahir', 'tanggal_lahir', 'pendidikan_terakhir', 'status_perkawinan', + 'komoditas_id', 'desa_kelurahan_id', + ]; + + private const ISIAN_LAHAN = [ // 18 + 3 wilayah = 21, per lahan + 'desa_kelurahan_id', 'path', 'luas_lahan', 'komoditas_id', 'jenis_komoditas_id', 'varietas_id', + 'jumlah_tanaman', 'kondisi_tanaman', 'serangan_hama', 'penyakit_tanaman', 'keparahan_serangan', + 'jenis_hama', 'jenis_penyakit', 'umur_tanaman', 'umur_lahan', 'komoditas_lain', 'tanaman_penaung', 'foto', + ]; + + private const ISIAN_PRAKTIK_BUDIDAYA = [ // 39 + 'tenaga_kerja', 'frekuensi_pemangkasan_utama', 'teknik_pemangkasan', 'frekuensi_pemangkasan_penaung', + 'penyerbukan_manual', 'deskripsi_penyerbukan_manual', 'tingkat_keberhasilan_penyerbukan', + 'pemberian_pupuk', 'jenis_pupuk', 'frekuensi_pemberian_pupuk', 'estimasi_dosis_pupuk_organik', + 'estimasi_dosis_pupuk_anorganik', 'waktu_pemberian_pupuk', 'cara_pemberian_pupuk', + 'metode_penanganan_opt', 'jenis_hama', 'jenis_penyakit', 'intensitas_serangan', 'waktu_kemunculan_terparah', + 'metode_paling_sering_dilakukan', 'volume_obat_obatan_padat_permusim', 'volume_obat_obatan_cair_permusim', + 'frekuensi_sanitasi_lahan', 'penanganan_sisa_buah_ranting', 'penanganan_gulma', + 'kondisi_pengelolaan_air', 'sumber_air', 'sistem_irigasi', 'kebutuhan_air_permusim', + 'kebiasaan_konservasi_tanah', 'pemahaman_manfaat_konservasi_tanah', + 'upah_tenaga_kerja', 'biaya_bibit', 'biaya_pupuk', 'biaya_obat_obatan', 'biaya_sewa_alat_produksi', + 'biaya_penyediaan_air', 'biaya_produksi_lainnya', 'total_estimasi_biaya_produksi', + ]; + + private const ISIAN_PANEN = [ // 21 + 'dasar_penentuan_panen', 'frekuensi_panen_pertahun', 'bulan_panen_utama', 'hasil_komoditas_utama_permusim', + 'hasil_komoditas_lainnya_permusim', 'disimpan_untuk_sendiri', 'hasil_panen_disimpan', 'tujuan_disimpan', + 'praktik_sortasi', 'bentuk_sortasi', 'hasil_panen_reject', 'jumlah_panen_reject', 'tindakan_panen_reject', + 'upah_tenaga_panen', 'biaya_pengangkutan_panen', 'biaya_wadah_panen', 'biaya_sewa_alat_panen', + 'biaya_panen_lainnya', 'total_estimasi_biaya_panen', 'estimasi_kehilangan_akibat_opt', + 'estimasi_kehilangan_akibat_hama_penyakit', + ]; + + private const ISIAN_PRODUKTIVITAS = ['komoditas_id', 'kuantitas', 'satuan']; // 3 (repeater) + private const ISIAN_PELATIHAN = ['jenis_pelatihan', 'penyelenggara', 'tahun_pelaksanaan']; // 3 (repeater) + private const ISIAN_SERTIFIKASI = ['jenis_sertifikasi', 'lembaga_penerbit', 'tahun_pelaksanaan', 'is_aktif']; // 4 (repeater) + + private const ISIAN_SOSIAL_EKONOMI = [ // 70 + 4 bantuan NGO + 4 pendapatan komoditas + 2 pendapatan luar = 80 + 'jumlah_tanggungan', 'anak_masih_sekolah', 'yang_ikut_bekerja', 'skor_periode_paceklik', 'bulan_paceklik', + 'sumber_penghasilan_paceklik', 'total_estimasi_biaya_kebun_per_musim', 'skor_kemampuan_menabung', + 'estimasi_tabungan_per_bulan', 'skor_kecukupan_penghasilan', 'alasan_kecukupan_penghasilan', + 'status_kepemilikan_lahan', 'skor_kejelasan_status_lahan', 'ada_sertifikat_dokumen', 'ada_sengketa', + 'keterangan_sengketa', 'pihak_membantu_konflik', 'pihak_membantu_konflik_nama', 'tahun_bantuan_konflik', + 'status_konflik', 'kemauan_mencoba_praktik_baru', 'alasan_mencoba_praktik_baru', + 'hambatan_mengubah_cara_bertani', 'pemahaman_mutu_harga', 'contoh_pemahaman_mutu_harga', + 'pengetahuan_harga_hasil_olah', 'pernah_jual_harga_lebih_tinggi', 'penyebab_harga_lebih_tinggi', + 'usaha_mutu_tambahan_harga', 'alasan_usaha_mutu_tambahan_harga', 'panutan_petani_sukses', + 'nama_lokasi_panutan', 'kemauan_ikut_program', 'harapan_program', 'orientasi_rencana_jangka_panjang', + 'rencana_jangka_panjang', 'terdaftar_kelompok_tani', 'nama_kelompok_tani', 'lama_bergabung_kelompok_tahun', + 'kedekatan_sesama_anggota', 'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok', + 'manfaat_kelompok_tani', 'status_kartu_tani', 'bisa_beli_pupuk_bersubsidi', 'manfaat_lain_kartu_tani', + 'status_rekening_tabungan', 'nama_bank', 'pengalaman_kredit_kebun', 'alasan_kredit_ditolak', + 'kondisi_aset_produktif', 'jenis_aset_produktif', 'keterangan_kondisi_aset', + 'keterlibatan_program_keuangan_formal', 'status_program_keuangan_formal', 'pernah_bantuan_ngo_perusahaan', + 'keterikatan_hutang_panen', 'pinjaman_tokoh_adat_panen', 'keterangan_pinjaman_tokoh_adat', + 'total_hutang_rupiah', 'total_hutang_setara_kg_panen', 'total_hutang_tidak_bersedia', 'praktik_ijon', + 'tekanan_jual_segera', 'penyebab_tekanan_jual_segera', 'kebebasan_memilih_pembeli', + 'alasan_tidak_bebas_memilih_pembeli', 'minat_pembeli_harga_stabil', 'alasan_minat_pembeli_harga_stabil', + ]; + private const ISIAN_BANTUAN_NGO = ['nama_lembaga', 'bentuk_bantuan', 'frekuensi', 'tahun_terakhir']; + private const ISIAN_PENDAPATAN_KOMODITAS = ['kategori', 'komoditas', 'pendapatan', 'periode']; + private const ISIAN_PENDAPATAN_LUAR = ['jenis_pekerjaan', 'estimasi_per_bulan']; + + private const ISIAN_AKSES_PASAR = [ // 16 + 4 harga jual = 20 + 'tujuan_penjualan', 'sifat_hubungan_bisnis', 'bentuk_penjualan', 'frekuensi_penjualan', 'skor_pengalaman', + 'saluran_terdahulu', 'alasan_berhenti', 'tahu_harga_sebelumnya', 'sumber_info_harga', 'praktik_fermentasi', + 'proses_fermentasi', 'model_fermentasi_memungkinkan', 'skor_kesadaran_premium', 'sumber_info_premium', + 'tingkat_kepercayaan_pembeli_baru', 'alasan_kepercayaan_pembeli_baru', + ]; + private const ISIAN_HARGA_JUAL = ['bentuk_produk', 'harga_panen_terakhir', 'harga_tahun_lalu', 'harga_dua_tahun_lalu']; + + /** + * Satu instance per objek Petani, supaya header dan ringkasan tidak memuat/menghitung ulang. + * WeakMap: otomatis dibuang saat objek Petani tidak dipakai lagi (tidak menumpuk antar-request). + */ + private static ?\WeakMap $cache = null; + + private ?Collection $semuaKosong = null; + private ?array $hitungIsian = null; + + public function __construct(private Petani $petani) + { + $petani->loadMissing([ + 'komoditas', + 'lahans', + 'praktikBudidaya', + 'surveyPanen', + 'surveyProduktivitas', + 'surveyPelatihan', + 'surveySertifikasi', + 'sosialEkonomi.pendapatanKomoditas', + 'sosialEkonomi.pendapatanLuarPertanian', + 'aksesPasar', + 'hargaJualPanen', + ]); + } + + public static function untuk(Petani $petani): self + { + self::$cache ??= new \WeakMap(); + + return self::$cache[$petani] ??= new self($petani); + } + + /** Jumlah isian WAJIB (skor) yang masih kosong di semua tab. */ + public function jumlahWajibKosong(): int + { + return $this->semuaKosong()->flatten(1)->where('prioritas', self::WAJIB)->count(); + } + + /** + * Hitungan seluruh isian: terisi, total (209 untuk 1 lahan; +21 per lahan tambahan), + * tidak_berlaku (pertanyaan lanjutan yang tidak perlu dijawab), berlaku, persen. + */ + public function hitungIsian(): array + { + return $this->hitungIsian ??= $this->hitungSemuaIsian(); + } + + private function hitungSemuaIsian(): array + { + $hasil = ['terisi' => 0, 'total' => 0, 'tidak_berlaku' => 0]; + $p = $this->petani; + + // Profil (+ provinsi/kabupaten/kecamatan yang mengikuti desa) + $this->hitungKolom($hasil, $p, self::ISIAN_PROFIL); + $this->hitungNilai($hasil, 3, self::terisi($p->desa_kelurahan_id)); + + // Lahan & Tanaman, per lahan. Belum ada lahan = dihitung 1 lahan kosong. + $daftarLahan = $p->lahans->isEmpty() ? collect([null]) : $p->lahans; + foreach ($daftarLahan as $lahan) { + $this->hitungKolom($hasil, $lahan, self::ISIAN_LAHAN); + $this->hitungNilai($hasil, 3, self::terisi($lahan?->desa_kelurahan_id)); + } + + // Praktik Budidaya, Panen, Produktivitas + $pb = $p->praktikBudidaya; + $tidakBerlakuPb = []; + if (! $p->isKomoditasVanili()) { + array_push($tidakBerlakuPb, 'penyerbukan_manual', 'deskripsi_penyerbukan_manual', 'tingkat_keberhasilan_penyerbukan'); + } + if ((int) $pb?->pemberian_pupuk === PemberianPupuk::TIDAK_PERNAH->value) { + array_push($tidakBerlakuPb, 'jenis_pupuk', 'frekuensi_pemberian_pupuk', 'estimasi_dosis_pupuk_organik', + 'estimasi_dosis_pupuk_anorganik', 'waktu_pemberian_pupuk', 'cara_pemberian_pupuk'); + } + if ((int) $pb?->metode_penanganan_opt === MetodePenangananHamaPenyakit::TIDAK_ADA->value) { + array_push($tidakBerlakuPb, 'jenis_hama', 'jenis_penyakit'); + } + $this->hitungKolom($hasil, $pb, self::ISIAN_PRAKTIK_BUDIDAYA, $tidakBerlakuPb); + $this->hitungKolom($hasil, $p->surveyPanen, self::ISIAN_PANEN); + $this->hitungRepeater($hasil, $p->surveyProduktivitas, self::ISIAN_PRODUKTIVITAS); + + // Pendidikan & Pelatihan + $this->hitungRepeater($hasil, $p->surveyPelatihan, self::ISIAN_PELATIHAN); + $this->hitungRepeater($hasil, $p->surveySertifikasi, self::ISIAN_SERTIFIKASI); + + // Sosial Ekonomi (data kelompok tidak berlaku kalau bukan anggota) + $se = $p->sosialEkonomi; + $tidakBerlakuSe = self::nilaiEnum($se?->terdaftar_kelompok_tani) === TerdaftarKelompokTani::TIDAK->value + ? ['nama_kelompok_tani', 'lama_bergabung_kelompok_tahun', 'kedekatan_sesama_anggota', + 'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok', 'manfaat_kelompok_tani'] + : []; + $this->hitungKolom($hasil, $se, self::ISIAN_SOSIAL_EKONOMI, $tidakBerlakuSe); + $this->hitungRepeater($hasil, collect($se?->detail_bantuan_ngo_perusahaan ?? []), self::ISIAN_BANTUAN_NGO); + $this->hitungRepeater($hasil, $se?->pendapatanKomoditas ?? collect(), self::ISIAN_PENDAPATAN_KOMODITAS); + $this->hitungRepeater($hasil, $se?->pendapatanLuarPertanian ?? collect(), self::ISIAN_PENDAPATAN_LUAR); + + // Akses Pasar (detail fermentasi tidak berlaku kalau belum pernah) + $ap = $p->aksesPasar; + $tidakBerlakuAp = self::nilaiEnum($ap?->praktik_fermentasi) === PraktikFermentasi::BELUM_PERNAH->value + ? ['proses_fermentasi', 'model_fermentasi_memungkinkan'] + : []; + $this->hitungKolom($hasil, $ap, self::ISIAN_AKSES_PASAR, $tidakBerlakuAp); + $this->hitungRepeater($hasil, $p->hargaJualPanen, self::ISIAN_HARGA_JUAL); + + $hasil['berlaku'] = $hasil['total'] - $hasil['tidak_berlaku']; + $hasil['persen'] = $hasil['berlaku'] > 0 ? (int) floor($hasil['terisi'] / $hasil['berlaku'] * 100) : 0; + + return $hasil; + } + + private static function nilaiEnum(mixed $nilai): mixed + { + return $nilai instanceof \BackedEnum ? $nilai->value : $nilai; + } + + private function hitungKolom(array &$hasil, ?object $model, array $kolom, array $tidakBerlaku = []): void + { + foreach ($kolom as $k) { + $hasil['total']++; + + if (in_array($k, $tidakBerlaku, true)) { + $hasil['tidak_berlaku']++; + } elseif ($model && self::terisi($model->{$k})) { + $hasil['terisi']++; + } + } + } + + private function hitungNilai(array &$hasil, int $jumlah, bool $terisi): void + { + $hasil['total'] += $jumlah; + $hasil['terisi'] += $terisi ? $jumlah : 0; + } + + /** Kolom repeater dianggap terisi kalau minimal satu baris mengisinya. */ + private function hitungRepeater(array &$hasil, Collection $baris, array $kolom): void + { + foreach ($kolom as $k) { + $hasil['total']++; + $hasil['terisi'] += $baris->contains(fn ($row) => self::terisi(data_get($row, $k))) ? 1 : 0; + } + } + + /** + * Isian yang masih kosong di satu tab. + * Setiap item: label, prioritas (wajib/perlu), input (atribut name di form), + * grup (judul pengelompokan, mis. nama lahan), lahan_id, url (opsional, halaman lain). + */ + public function kosong(string $tab): Collection + { + return $this->semuaKosong()->get($tab, collect()); + } + + private function hitungKosong(string $tab): Collection + { + $isian = match ($tab) { + 'profil' => $this->profil(), + 'lahan' => $this->lahan(), + 'praktik-budidaya' => $this->praktikBudidaya(), + 'sosial-ekonomi' => $this->sosialEkonomi(), + 'akses-pasar' => $this->aksesPasar(), + default => [], + }; + + return collect($isian) + ->reject(fn (array $item) => $item['terisi']) + ->map(fn (array $item) => collect($item)->except('terisi')->all()) + ->values(); + } + + /** Isian kosong semua tab, dikelompokkan per tab (untuk tab Kesiapan Adopsi). */ + public function semuaKosong(): Collection + { + return $this->semuaKosong ??= collect(array_keys(self::TAB_LABEL)) + ->mapWithKeys(fn (string $tab) => [$tab => $this->hitungKosong($tab)]); + } + + private function item(string $label, string $prioritas, string $input, mixed $nilai, array $tambahan = []): array + { + return $this->itemCek($label, $prioritas, $input, self::terisi($nilai), $tambahan); + } + + /** Untuk isian yang status terisinya dihitung sendiri (mis. repeater). */ + private function itemCek(string $label, string $prioritas, string $input, bool $terisi, array $tambahan = []): array + { + return [ + 'label' => $label, + 'prioritas' => $prioritas, + 'input' => $input, + 'terisi' => $terisi, + 'grup' => null, + 'lahan_id' => null, + 'url' => null, + ...$tambahan, + ]; + } + + private static function terisi(mixed $nilai): bool + { + if ($nilai instanceof Collection) { + return $nilai->isNotEmpty(); + } + + return ! (is_null($nilai) || $nilai === '' || $nilai === []); + } + + private function profil(): array + { + $p = $this->petani; + + return [ + $this->item('Jenis kelamin', self::PERLU, 'jenis_kelamin', $p->jenis_kelamin), + $this->item('Tanggal lahir', self::PERLU, 'tanggal_lahir', $p->tanggal_lahir), + $this->item('Pendidikan terakhir', self::PERLU, 'pendidikan_terakhir', $p->pendidikan_terakhir), + $this->item('Komoditas utama', self::PERLU, 'komoditas_id', $p->komoditas_id), + $this->item('Desa/kelurahan', self::PERLU, 'desa_kelurahan_id', $p->desa_kelurahan_id), + ]; + } + + /** Diisi per lahan; setiap item membawa link ke halaman edit lahannya. */ + private function lahan(): array + { + $daftarLahan = $this->petani->lahans->sortBy('id'); + + if ($daftarLahan->isEmpty()) { + return [$this->itemCek('Belum ada lahan', self::PERLU, '', false, [ + 'url' => route('petani.lahan-tanaman.create', $this->petani->id), + ])]; + } + + return $daftarLahan->values()->flatMap(function (Lahan $l, int $i) { + $tambahan = [ + 'grup' => $l->nama ?: 'Lahan ' . ($i + 1), + 'lahan_id' => $l->id, + 'url' => route('petani.lahan-tanaman.edit', [$this->petani->id, $l->id]), + ]; + $adaSerangan = self::terisi($l->serangan_hama) || self::terisi($l->penyakit_tanaman); + + return array_filter([ + $this->item('Luas lahan', self::PERLU, 'luas_lahan', $l->luas_lahan, $tambahan), + $this->item('Peta/polygon lahan', self::PERLU, 'path', $l->path, $tambahan), + $this->item('Desa lahan', self::PERLU, 'desa_kelurahan_id', $l->desa_kelurahan_id, $tambahan), + $this->item('Umur tanaman', self::PERLU, 'umur_tanaman', $l->umur_tanaman, $tambahan), + $this->item('Jumlah tanaman', self::PERLU, 'jumlah_tanaman', $l->jumlah_tanaman, $tambahan), + $this->item('Kondisi tanaman', self::PERLU, 'kondisi_tanaman', $l->kondisi_tanaman, $tambahan), + $this->item('Serangan hama', self::PERLU, 'serangan_hama', $l->serangan_hama, $tambahan), + $this->item('Penyakit tanaman', self::PERLU, 'penyakit_tanaman', $l->penyakit_tanaman, $tambahan), + $adaSerangan + ? $this->item('Keparahan serangan', self::PERLU, 'keparahan_serangan', $l->keparahan_serangan, $tambahan) + : null, + ]); + })->all(); + } + + private function praktikBudidaya(): array + { + $pb = $this->petani->praktikBudidaya; + $panen = $this->petani->surveyPanen; + + $adaHama = $pb && self::terisi($pb->metode_penanganan_opt) + && (int) $pb->metode_penanganan_opt !== MetodePenangananHamaPenyakit::TIDAK_ADA->value; + + $konservasi = (array) ($pb?->kebiasaan_konservasi_tanah ?? []); + $adaKonservasi = collect($konservasi)->contains(fn ($v) => $v !== self::KONSERVASI_TIDAK_ADA); + + return array_filter([ + $this->item('Frekuensi pemangkasan', self::WAJIB, 'frekuensi_pemangkasan_komoditas_utama', $pb?->frekuensi_pemangkasan_utama), + $this->item('Pemberian pupuk', self::WAJIB, 'pemberian_pupuk', $pb?->pemberian_pupuk), + $this->item('Metode penanganan hama & penyakit', self::WAJIB, 'metode_penanganan_hama_penyakit', $pb?->metode_penanganan_opt), + $adaHama ? $this->item('Jenis hama', self::PERLU, 'jenis_hama', $pb->jenis_hama) : null, + $adaHama ? $this->item('Jenis penyakit', self::PERLU, 'jenis_penyakit', $pb->jenis_penyakit) : null, + $this->item('Frekuensi sanitasi lahan', self::WAJIB, 'frekuensi_sanitasi_lahan', $pb?->frekuensi_sanitasi_lahan), + $this->item('Penanganan gulma', self::WAJIB, 'penanganan_gulma', $pb?->penanganan_gulma), + $this->item('Kondisi pengelolaan air & irigasi', self::WAJIB, 'kondisi_pengelolaan_air_irigasi', $pb?->kondisi_pengelolaan_air), + $this->item('Sumber air', self::PERLU, 'sumber_air', $pb?->sumber_air), + $this->item('Kebiasaan konservasi tanah', self::WAJIB, 'kebiasaan_konservasi_tanah', $konservasi), + $adaKonservasi + ? $this->item('Pemahaman manfaat konservasi', self::WAJIB, 'pemahaman_manfaat_konservasi', $pb->pemahaman_manfaat_konservasi_tanah) + : null, + $this->item('Total estimasi biaya produksi', self::WAJIB, 'total_estimasi_biaya_produksi', $pb?->total_estimasi_biaya_produksi), + $this->item('Dasar penentuan panen', self::WAJIB, 'dasar_penentuan_panen', $panen?->dasar_penentuan_panen), + $this->item('Hasil komoditas utama per musim', self::PERLU, 'hasil_komoditas_utama_permusim', $panen?->hasil_komoditas_utama_permusim), + $this->item('Praktik sortasi', self::WAJIB, 'praktik_sortasi', $panen?->praktik_sortasi), + ]); + } + + private function sosialEkonomi(): array + { + $se = $this->petani->sosialEkonomi; + $anggotaKelompok = $se?->terdaftar_kelompok_tani === TerdaftarKelompokTani::YA + || $se?->terdaftar_kelompok_tani === TerdaftarKelompokTani::YA->value; + + $adaPendapatan = (bool) $se?->pendapatanKomoditas->contains(fn ($row) => (int) $row->pendapatan > 0); + + return array_filter([ + // Utama + $this->item('Jumlah tanggungan', self::PERLU, 'jumlah_tanggungan', $se?->jumlah_tanggungan), + $this->itemCek('Pendapatan per komoditas', self::WAJIB, 'pendapatan_komoditas', $adaPendapatan), + $this->item('Periode paceklik', self::WAJIB, 'skor_periode_paceklik', $se?->skor_periode_paceklik), + $this->item('Kemampuan menabung', self::WAJIB, 'skor_kemampuan_menabung', $se?->skor_kemampuan_menabung), + $this->item('Kecukupan penghasilan', self::WAJIB, 'skor_kecukupan_penghasilan', $se?->skor_kecukupan_penghasilan), + $this->item('Kejelasan status lahan', self::WAJIB, 'skor_kejelasan_status_lahan', $se?->skor_kejelasan_status_lahan), + + // Sosial budaya + $this->item('Kemauan mencoba praktik baru', self::WAJIB, 'kemauan_mencoba_praktik_baru', $se?->kemauan_mencoba_praktik_baru), + $this->item('Pemahaman mutu & harga', self::WAJIB, 'pemahaman_mutu_harga', $se?->pemahaman_mutu_harga), + $this->item('Panutan petani sukses', self::WAJIB, 'panutan_petani_sukses', $se?->panutan_petani_sukses), + $this->item('Kemauan ikut program', self::WAJIB, 'kemauan_ikut_program', $se?->kemauan_ikut_program), + $this->item('Orientasi rencana jangka panjang', self::WAJIB, 'orientasi_rencana_jangka_panjang', $se?->orientasi_rencana_jangka_panjang), + + // Keanggotaan kelompok: pertanyaan lanjutan hanya untuk anggota + $this->item('Terdaftar di kelompok tani', self::WAJIB, 'terdaftar_kelompok_tani', $se?->terdaftar_kelompok_tani), + $anggotaKelompok ? $this->item('Keaktifan pertemuan kelompok', self::WAJIB, 'keaktifan_pertemuan_kelompok', $se->keaktifan_pertemuan_kelompok) : null, + $anggotaKelompok ? $this->item('Peran dalam kelompok', self::WAJIB, 'peran_dalam_kelompok', $se->peran_dalam_kelompok) : null, + $anggotaKelompok ? $this->item('Manfaat kelompok tani', self::WAJIB, 'manfaat_kelompok_tani', $se->manfaat_kelompok_tani) : null, + + // Akses keuangan + $this->item('Status rekening tabungan', self::WAJIB, 'status_rekening_tabungan', $se?->status_rekening_tabungan), + $this->item('Pengalaman kredit kebun', self::WAJIB, 'pengalaman_kredit_kebun', $se?->pengalaman_kredit_kebun), + $this->item('Kondisi aset produktif', self::WAJIB, 'kondisi_aset_produktif', $se?->kondisi_aset_produktif), + $this->item('Keterlibatan program keuangan formal', self::WAJIB, 'keterlibatan_program_keuangan_formal', $se?->keterlibatan_program_keuangan_formal), + + // Keterikatan hutang + $this->item('Keterikatan hutang panen', self::WAJIB, 'keterikatan_hutang_panen', $se?->keterikatan_hutang_panen), + $this->item('Praktik ijon', self::WAJIB, 'praktik_ijon', $se?->praktik_ijon), + $this->item('Kebebasan memilih pembeli', self::WAJIB, 'kebebasan_memilih_pembeli', $se?->kebebasan_memilih_pembeli), + ]); + } + + private function aksesPasar(): array + { + $ap = $this->petani->aksesPasar; + $harga = $this->petani->hargaJualPanen; + + return [ + $this->item('Tujuan penjualan', self::PERLU, 'tujuan_penjualan', $ap?->tujuan_penjualan), + $this->item('Sifat hubungan dengan pembeli', self::PERLU, 'sifat_hubungan_bisnis', $ap?->sifat_hubungan_bisnis), + $this->item('Frekuensi penjualan', self::PERLU, 'frekuensi_penjualan', $ap?->frekuensi_penjualan), + $this->item('Pengalaman menjual ke saluran lain', self::WAJIB, 'skor_pengalaman', $ap?->skor_pengalaman), + $this->item('Praktik fermentasi', self::PERLU, 'praktik_fermentasi', $ap?->praktik_fermentasi), + $this->item('Kesadaran pasar premium', self::WAJIB, 'skor_kesadaran_premium', $ap?->skor_kesadaran_premium), + $this->item('Kepercayaan pada pembeli baru', self::WAJIB, 'tingkat_kepercayaan_pembeli_baru', $ap?->tingkat_kepercayaan_pembeli_baru), + $this->itemCek('Harga jual panen terakhir', self::PERLU, 'harga_jual', + $harga->contains(fn ($h) => self::terisi($h->bentuk_produk) && (float) $h->harga_panen_terakhir > 0)), + $this->itemCek('Harga jual tahun lalu', self::PERLU, 'harga_jual', + $harga->contains(fn ($h) => (float) $h->harga_tahun_lalu > 0)), + $this->itemCek('Harga jual dua tahun lalu', self::PERLU, 'harga_jual', + $harga->contains(fn ($h) => (float) $h->harga_dua_tahun_lalu > 0)), + ]; + } +} diff --git a/database/migrations/2026_11_04_100002_tambah_status_ke_petani_table.php b/database/migrations/2026_11_04_100002_tambah_status_ke_petani_table.php new file mode 100644 index 0000000..869ca90 --- /dev/null +++ b/database/migrations/2026_11_04_100002_tambah_status_ke_petani_table.php @@ -0,0 +1,27 @@ +string('status', 20)->default('draft')->index(); + }); + } + + public function down(): void + { + Schema::table('petani', function (Blueprint $table) { + $table->dropIndex(['status']); + $table->dropColumn('status'); + }); + } +}; diff --git a/resources/views/components/kelengkapan-petani.blade.php b/resources/views/components/kelengkapan-petani.blade.php new file mode 100644 index 0000000..1fc1f92 --- /dev/null +++ b/resources/views/components/kelengkapan-petani.blade.php @@ -0,0 +1,197 @@ +{{-- + Ringkasan isian WAJIB (skor) dan PERLU (dashboard) yang belum terisi di tab petani yang sedang dibuka. + Daftar isiannya ada di App\Services\Produktivitas\KelengkapanPetani. + Klik isian → halaman menggulir ke kolomnya (dicari lewat atribut name). +--}} +@props(['petani', 'lahan' => null]) + +@use('App\Services\Produktivitas\KelengkapanPetani') + +@php + $tab = match (true) { + request()->routeIs('petani.edit') => 'profil', + request()->routeIs('petani.lahan-tanaman.*') => 'lahan', + request()->routeIs('petani.praktik-budidaya.*') => 'praktik-budidaya', + request()->routeIs('petani.sosial-ekonomi.*') => 'sosial-ekonomi', + request()->routeIs('petani.akses-pasar.*') => 'akses-pasar', + request()->routeIs('petani.skor.*') => 'semua', + default => null, + }; + + $urlTab = fn (string $t) => $t === 'profil' + ? route('petani.edit', $petani->id) + : route('petani.' . ($t === 'lahan' ? 'lahan-tanaman' : $t) . '.index', $petani->id); + + $kelengkapan = $tab ? KelengkapanPetani::untuk($petani) : null; + + if ($tab === 'semua') { + // Tab Kesiapan Adopsi: rangkuman semua tab, klik → pindah ke tab terkait. + $kelompok = $kelengkapan->semuaKosong() + ->map(fn ($items, $t) => $items->map(fn ($item) => [ + ...$item, + 'url' => ($item['url'] ?? $urlTab($t)) . ($item['input'] ? '#isian=' . $item['input'] : ''), + ])) + ->filter(fn ($items) => $items->isNotEmpty()) + ->mapWithKeys(fn ($items, $t) => [KelengkapanPetani::TAB_LABEL[$t] => $items]); + } elseif ($tab) { + // Di halaman edit lahan, isian lahan yang sedang dibuka langsung digulir (tanpa pindah halaman). + $lahanDibukaId = $tab === 'lahan' && request()->routeIs('petani.lahan-tanaman.edit') ? $lahan?->id : null; + + $kelompok = $kelengkapan->kosong($tab) + ->map(function ($item) use ($lahanDibukaId) { + $dibuka = $lahanDibukaId && $item['lahan_id'] === $lahanDibukaId; + + return [ + ...$item, + 'grup' => $dibuka ? $item['grup'] . ' (sedang dibuka)' : $item['grup'], + 'url' => ($dibuka || ! $item['url']) ? null : $item['url'] . ($item['input'] ? '#isian=' . $item['input'] : ''), + ]; + }) + ->groupBy(fn ($item) => $item['grup'] ?? ''); + } + + $semuaItem = $tab ? $kelompok->flatten(1) : collect(); + $jumlahWajib = $semuaItem->where('prioritas', KelengkapanPetani::WAJIB)->count(); + $jumlahPerlu = $semuaItem->where('prioritas', KelengkapanPetani::PERLU)->count(); +@endphp + +@if ($tab) + @if ($semuaItem->isEmpty()) + + @else + {{-- Klik di mana saja pada kartu untuk membuka/menutup detail (kecuali tombol isian). --}} + + @endif + + @once + @push('style') + + @endpush + + @push('scripts') + + @endpush + @endonce +@endif diff --git a/resources/views/components/status-petani.blade.php b/resources/views/components/status-petani.blade.php new file mode 100644 index 0000000..95d58f1 --- /dev/null +++ b/resources/views/components/status-petani.blade.php @@ -0,0 +1,102 @@ +{{-- + Kelengkapan data (terisi X dari 209) + status Draf/Lengkap/Terverifikasi di kartu Profil Petani. + Tombol "Tandai Lengkap" hanya untuk enumerator penginput, dan nonaktif selama isian wajib masih kosong. +--}} +@props(['petani']) + +@use('App\Enums\StatusDataPetani') +@use('App\Services\Produktivitas\KelengkapanPetani') + +@php + $kelengkapan = KelengkapanPetani::untuk($petani); + $hitung = $kelengkapan->hitungIsian(); + $wajibKosong = $kelengkapan->jumlahWajibKosong(); + $status = $petani->status ?? StatusDataPetani::DRAFT; + $bisaTandai = $petani->bisaDitandaiOleh(auth()->user()); +@endphp + +
+
+
Kelengkapan Data
+ {{ $status->label() }} +
+ +
+ {{ $hitung['terisi'] }} dari {{ $hitung['total'] }} isian terisi + {{ $hitung['persen'] }}% +
+
+
+
+

+ @if ($hitung['tidak_berlaku']) + {{ $hitung['tidak_berlaku'] }} isian tidak berlaku untuk petani ini. + @endif + @if ($wajibKosong) + {{ $wajibKosong }} isian wajib masih kosong. + @else + Semua isian wajib sudah terisi. + @endif +

+ +@if ($bisaTandai && $status === StatusDataPetani::DRAFT) +
+ @csrf + @method('PUT') + + +
+@elseif ($bisaTandai && $status === StatusDataPetani::LENGKAP) +
+ @csrf + @method('PUT') + + +
+@elseif ($status === StatusDataPetani::DRAFT) +

Hanya enumerator penginput yang dapat menandai lengkap.

+@elseif ($status === StatusDataPetani::LENGKAP) +

Menunggu verifikasi koordinator.

+@endif + +@once + @push('scripts') + + @endpush +@endonce diff --git a/resources/views/layout/main.blade.php b/resources/views/layout/main.blade.php index 25c5a1e..ecdf6a5 100644 --- a/resources/views/layout/main.blade.php +++ b/resources/views/layout/main.blade.php @@ -113,6 +113,41 @@ } } --> + + {{-- Tombol kembali ke atas: muncul setelah halaman digulir ke bawah. --}} + + + + @stack('scripts')