Update Alert Profil Petani
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
// Status kelengkapan data profil petani.
|
||||
// Draf → Lengkap (ditandai enumerator penginput) → Terverifikasi (oleh koordinator, menu menyusul).
|
||||
enum StatusDataPetani: string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case LENGKAP = 'lengkap';
|
||||
case TERVERIFIKASI = 'terverifikasi';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => '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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Produktivitas;
|
||||
|
||||
use App\Enums\StatusDataPetani;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Services\Produktivitas\KelengkapanPetani;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Enumerator penginput menandai data petani Lengkap, atau membatalkannya kembali ke Draf.
|
||||
* Perubahan ke Terverifikasi dilakukan koordinator (menu menyusul), bukan di sini.
|
||||
*/
|
||||
class PetaniStatusController extends Controller
|
||||
{
|
||||
public function update(Request $request, Petani $petani)
|
||||
{
|
||||
abort_unless($petani->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.');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Produktivitas;
|
||||
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
|
||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||
use App\Enums\SosialEkonomi\TerdaftarKelompokTani;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Daftar isian Profil Petani yang WAJIB (dipakai skor Kesiapan Adopsi)
|
||||
* dan PERLU (dipakai dashboard Karakteristik, Produksi, Pasar).
|
||||
*
|
||||
* Satu-satunya tempat daftar ini ditulis. Dipakai oleh ringkasan kelengkapan
|
||||
* di atas setiap tab petani, dan nanti oleh fitur verifikasi.
|
||||
*
|
||||
* Isian yang tidak berlaku (mis. jenis hama kalau "tidak ada hama",
|
||||
* data kelompok kalau bukan anggota) tidak dimasukkan.
|
||||
*/
|
||||
class KelengkapanPetani
|
||||
{
|
||||
public const WAJIB = 'wajib';
|
||||
public const PERLU = 'perlu';
|
||||
|
||||
public const TAB_LABEL = [
|
||||
'profil' => '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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Status kelengkapan data petani (App\Enums\StatusDataPetani):
|
||||
* draft → lengkap → terverifikasi. Data lama otomatis "draft".
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('petani', function (Blueprint $table) {
|
||||
$table->string('status', 20)->default('draft')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('petani', function (Blueprint $table) {
|
||||
$table->dropIndex(['status']);
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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())
|
||||
<div class="alert alert-success d-flex align-items-center py-2 mb-6" role="alert">
|
||||
<i class="icon-base ti tabler-circle-check me-2"></i>
|
||||
{{ $tab === 'semua' ? 'Semua isian wajib dan perlu sudah terisi.' : 'Semua isian wajib dan perlu di tab ini sudah terisi.' }}
|
||||
</div>
|
||||
@else
|
||||
{{-- Klik di mana saja pada kartu untuk membuka/menutup detail (kecuali tombol isian). --}}
|
||||
<div class="card mb-6 border border-warning kartu-kelengkapan" id="kelengkapan-petani"
|
||||
role="button" aria-expanded="false" aria-controls="kelengkapan-petani-detail" title="Klik untuk menampilkan/menyembunyikan detail">
|
||||
<div class="card-body py-3">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="icon-base ti tabler-alert-triangle text-warning me-2"></i>
|
||||
<span class="fw-medium">
|
||||
Belum terisi{{ $tab === 'semua' ? '' : ' di tab ini' }}:
|
||||
@if ($jumlahWajib)
|
||||
<span class="badge bg-label-danger ms-1">{{ $jumlahWajib }} wajib</span>
|
||||
@endif
|
||||
@if ($jumlahPerlu)
|
||||
<span class="badge bg-label-warning ms-1">{{ $jumlahPerlu }} perlu</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<span class="btn btn-sm btn-icon btn-text-secondary rounded-pill ikon-toggle-kelengkapan" aria-hidden="true">
|
||||
<i class="icon-base ti tabler-chevron-down"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="collapse" id="kelengkapan-petani-detail">
|
||||
<small class="text-muted d-block mt-2">
|
||||
<span class="badge badge-dot bg-danger me-1"></span>Wajib: dipakai skor Kesiapan Adopsi
|
||||
<span class="badge badge-dot bg-warning ms-3 me-1"></span>Perlu: dipakai dashboard.
|
||||
Klik isian untuk menuju kolomnya.
|
||||
</small>
|
||||
|
||||
@foreach ($kelompok as $judul => $items)
|
||||
<div class="mt-3">
|
||||
@if ($judul !== '')
|
||||
<div class="small fw-medium mb-1">{{ $judul }}</div>
|
||||
@endif
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach ($items->sortBy(fn ($i) => $i['prioritas'] === KelengkapanPetani::WAJIB ? 0 : 1) as $item)
|
||||
<button type="button"
|
||||
class="btn btn-xs btn-isian-kosong {{ $item['prioritas'] === KelengkapanPetani::WAJIB ? 'btn-label-danger' : 'btn-label-warning' }}"
|
||||
data-input="{{ $item['input'] }}"
|
||||
data-url="{{ $item['url'] }}">
|
||||
{{ $item['label'] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@once
|
||||
@push('style')
|
||||
<style>
|
||||
.kartu-kelengkapan { cursor: pointer; }
|
||||
.kartu-kelengkapan .btn-isian-kosong { cursor: pointer; }
|
||||
.ikon-toggle-kelengkapan i { transition: transform .2s; }
|
||||
.kartu-kelengkapan.terbuka .ikon-toggle-kelengkapan i { transform: rotate(180deg); }
|
||||
.isian-disorot {
|
||||
outline: 2px solid var(--bs-warning);
|
||||
outline-offset: 4px;
|
||||
border-radius: .375rem;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function () {
|
||||
function cariKolom(name) {
|
||||
if (!name) return null;
|
||||
const el = document.querySelector(`[name="${name}"], [name="${name}[]"], [name^="${name}["]`);
|
||||
if (!el) return null;
|
||||
|
||||
// Kolom tersembunyi (mis. radio custom/select2): sorot pembungkus terdekat yang terlihat.
|
||||
let target = el.closest('.col-md-12, .col-md-6, .col-md-4, .col-12, .col, .mb-4, .mb-6') || el;
|
||||
while (target && target.offsetParent === null && target.parentElement) {
|
||||
target = target.parentElement;
|
||||
}
|
||||
return { el, target };
|
||||
}
|
||||
|
||||
function sorot(name) {
|
||||
const hasil = cariKolom(name);
|
||||
if (!hasil) return false;
|
||||
|
||||
hasil.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
hasil.target.classList.add('isian-disorot');
|
||||
setTimeout(() => hasil.target.classList.remove('isian-disorot'), 2500);
|
||||
if (hasil.el.offsetParent !== null && typeof hasil.el.focus === 'function') {
|
||||
hasil.el.focus({ preventScroll: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Buka/tutup detail dengan klik di mana saja pada kartu.
|
||||
const kartu = document.getElementById('kelengkapan-petani');
|
||||
const detail = document.getElementById('kelengkapan-petani-detail');
|
||||
if (kartu && detail) {
|
||||
kartu.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.btn-isian-kosong')) return;
|
||||
if (window.getSelection().toString()) return; // sedang menyeleksi teks
|
||||
bootstrap.Collapse.getOrCreateInstance(detail, { toggle: false }).toggle();
|
||||
});
|
||||
detail.addEventListener('show.bs.collapse', () => { kartu.classList.add('terbuka'); kartu.setAttribute('aria-expanded', 'true'); });
|
||||
detail.addEventListener('hide.bs.collapse', () => { kartu.classList.remove('terbuka'); kartu.setAttribute('aria-expanded', 'false'); });
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-isian-kosong');
|
||||
if (!btn) return;
|
||||
|
||||
if (btn.dataset.url) {
|
||||
window.location.href = btn.dataset.url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sorot(btn.dataset.input)) {
|
||||
const pesan = 'Kolom ini belum tampil di halaman ini. Simpan bagian yang sudah ada terlebih dahulu, atau periksa jawaban pertanyaan sebelumnya.';
|
||||
window.Swal ? Swal.fire({ icon: 'info', text: pesan }) : alert(pesan);
|
||||
}
|
||||
});
|
||||
|
||||
// Datang dari tab lain dengan #isian=nama_kolom
|
||||
window.addEventListener('load', function () {
|
||||
const cocok = window.location.hash.match(/^#isian=(.+)$/);
|
||||
if (cocok) setTimeout(() => sorot(decodeURIComponent(cocok[1])), 300);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@endif
|
||||
@@ -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
|
||||
|
||||
<hr class="my-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<h6 class="mb-0" style="font-size: 0.85rem;">Kelengkapan Data</h6>
|
||||
<span class="badge bg-label-{{ $status->warna() }}">{{ $status->label() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between" style="font-size: 0.75rem;">
|
||||
<span>{{ $hitung['terisi'] }} dari {{ $hitung['total'] }} isian terisi</span>
|
||||
<span class="fw-medium">{{ $hitung['persen'] }}%</span>
|
||||
</div>
|
||||
<div class="progress my-1" style="height: 6px;">
|
||||
<div class="progress-bar {{ $hitung['persen'] >= 100 ? 'bg-success' : '' }}" role="progressbar"
|
||||
style="width: {{ $hitung['persen'] }}%;" aria-valuenow="{{ $hitung['persen'] }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<p class="mb-0 text-muted" style="font-size: 0.7rem;">
|
||||
@if ($hitung['tidak_berlaku'])
|
||||
{{ $hitung['tidak_berlaku'] }} isian tidak berlaku untuk petani ini.
|
||||
@endif
|
||||
@if ($wajibKosong)
|
||||
<span class="text-danger">{{ $wajibKosong }} isian wajib masih kosong.</span>
|
||||
@else
|
||||
<span class="text-success">Semua isian wajib sudah terisi.</span>
|
||||
@endif
|
||||
</p>
|
||||
|
||||
@if ($bisaTandai && $status === StatusDataPetani::DRAFT)
|
||||
<form method="POST" action="{{ route('petani.status.update', $petani->id) }}" class="form-status-petani mt-2"
|
||||
data-konfirmasi="Tandai data petani ini sudah lengkap? Koordinator akan memverifikasinya.">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="{{ StatusDataPetani::LENGKAP->value }}">
|
||||
<button type="submit" class="btn btn-sm btn-success w-100" @disabled($wajibKosong > 0)
|
||||
@if ($wajibKosong) title="Lengkapi {{ $wajibKosong }} isian wajib terlebih dahulu" @endif>
|
||||
<i class="icon-base ti tabler-checks icon-sm me-1"></i>Tandai Sudah Lengkap
|
||||
</button>
|
||||
</form>
|
||||
@elseif ($bisaTandai && $status === StatusDataPetani::LENGKAP)
|
||||
<form method="POST" action="{{ route('petani.status.update', $petani->id) }}" class="form-status-petani mt-2"
|
||||
data-konfirmasi="Batalkan tanda lengkap? Status kembali ke draf.">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="status" value="{{ StatusDataPetani::DRAFT->value }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary w-100">
|
||||
<i class="icon-base ti tabler-arrow-back-up icon-sm me-1"></i>Batalkan Tanda Lengkap
|
||||
</button>
|
||||
</form>
|
||||
@elseif ($status === StatusDataPetani::DRAFT)
|
||||
<p class="mb-0 mt-1 text-muted fst-italic" style="font-size: 0.7rem;">Hanya enumerator penginput yang dapat menandai lengkap.</p>
|
||||
@elseif ($status === StatusDataPetani::LENGKAP)
|
||||
<p class="mb-0 mt-1 text-muted fst-italic" style="font-size: 0.7rem;">Menunggu verifikasi koordinator.</p>
|
||||
@endif
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('submit', function (e) {
|
||||
const form = e.target.closest('.form-status-petani');
|
||||
if (!form || form.dataset.terkonfirmasi) return;
|
||||
|
||||
e.preventDefault();
|
||||
const kirim = () => { form.dataset.terkonfirmasi = '1'; form.submit(); };
|
||||
|
||||
if (!window.Swal) {
|
||||
if (confirm(form.dataset.konfirmasi)) kirim();
|
||||
return;
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'question',
|
||||
text: form.dataset.konfirmasi,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya',
|
||||
cancelButtonText: 'Batal',
|
||||
}).then((r) => { if (r.isConfirmed) kirim(); });
|
||||
});
|
||||
|
||||
@if (session('status_petani_success') || session('status_petani_error'))
|
||||
window.addEventListener('load', function () {
|
||||
const pesan = @json(session('status_petani_success') ?? session('status_petani_error'));
|
||||
const ikon = @json(session('status_petani_success') ? 'success' : 'error');
|
||||
window.Swal ? Swal.fire({ icon: ikon, text: pesan }) : alert(pesan);
|
||||
});
|
||||
@endif
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -113,6 +113,41 @@
|
||||
}
|
||||
}
|
||||
</style> -->
|
||||
|
||||
{{-- Tombol kembali ke atas: muncul setelah halaman digulir ke bawah. --}}
|
||||
<button type="button" id="btn-kembali-ke-atas" class="btn btn-primary btn-icon rounded-pill shadow"
|
||||
title="Kembali ke atas" aria-label="Kembali ke atas">
|
||||
<i class="icon-base ti tabler-arrow-up"></i>
|
||||
</button>
|
||||
<style>
|
||||
#btn-kembali-ke-atas {
|
||||
position: fixed;
|
||||
right: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
z-index: 1040;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(1rem);
|
||||
transition: opacity .25s, transform .25s, visibility .25s;
|
||||
}
|
||||
#btn-kembali-ke-atas.tampil {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const btn = document.getElementById('btn-kembali-ke-atas');
|
||||
const cek = () => btn.classList.toggle('tampil', window.scrollY > 300);
|
||||
window.addEventListener('scroll', cek, { passive: true });
|
||||
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
cek();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
<script>
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
<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 ?? '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>
|
||||
|
||||
@if ($petani)
|
||||
<x-status-petani :petani="$petani" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +72,7 @@
|
||||
$skorLahanTanaman = isset($petani) ? $petani->skorLahanTanaman() : null;
|
||||
|
||||
$lahanPetani = isset($petani) && \Illuminate\Support\Facades\Schema::hasTable('map_lahan')
|
||||
? $petani->lahans()->get()
|
||||
? $petani->lahans
|
||||
: collect();
|
||||
$totalLuasLahanHa = $petani ? $lahanPetani->where('komoditas_id', $petani->komoditas_id)->sum('luas_lahan') : 0;
|
||||
$lahanKomoditasUtamaIds = $petani
|
||||
@@ -393,3 +397,7 @@
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($petani)
|
||||
<x-kelengkapan-petani :petani="$petani" :lahan="$lahanSelected ?? null" />
|
||||
@endif
|
||||
|
||||
@@ -230,6 +230,7 @@ Route::middleware('auth')
|
||||
Route::get('petani/show-image/{id}/{field}', [App\Http\Controllers\Web\Produktivitas\PetaniController::class, 'showImage'])->name('petani.show-image');
|
||||
|
||||
Route::resource('petani', App\Http\Controllers\Web\Produktivitas\PetaniController::class)->whereNumber('petani');
|
||||
Route::singleton('petani.status', App\Http\Controllers\Web\Produktivitas\PetaniStatusController::class)->only(['update']);
|
||||
Route::get('petani/{petani}/akses-pasar', [App\Http\Controllers\Web\Produktivitas\AksesPasarController::class, 'editByPetani'])->name('petani.akses-pasar.edit');
|
||||
Route::get('petani/{petani}/lahan-tanaman/summary', [App\Http\Controllers\Web\Produktivitas\PetaniLahanController::class, 'summary'])->name('petani.lahan-tanaman.summary');
|
||||
Route::resource('petani.lahan-tanaman', App\Http\Controllers\Web\Produktivitas\PetaniLahanController::class)->whereNumber('petani');
|
||||
|
||||
Reference in New Issue
Block a user