Update Monitoring dan Verifikasi Petani
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Concerns;
|
||||||
|
|
||||||
|
use App\Models\Pertanian\Petani;
|
||||||
|
use App\Services\Produktivitas\KelengkapanPetani;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Collection as BaseCollection;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bantuan bersama menu Verifikasi > Petani (tab Monitoring & Verifikasi).
|
||||||
|
* Hanya super_admin, admin, dan koordinator; koordinator dibatasi komoditas yang ditugaskan.
|
||||||
|
*/
|
||||||
|
trait AksesMonitoringPetani
|
||||||
|
{
|
||||||
|
protected const PER_PAGE = [10, 25, 50];
|
||||||
|
|
||||||
|
protected function izinkan(): void
|
||||||
|
{
|
||||||
|
abort_unless(Gate::any(['super_admin', 'admin', 'koordinator']), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function izinkanPetani(Petani $petani): void
|
||||||
|
{
|
||||||
|
$this->izinkan();
|
||||||
|
abort_if(auth()->user()->isKoordinator() && ! auth()->user()->bolehAksesPetani($petani), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Query petani + relasi untuk hitung kelengkapan (eager load, tanpa N+1), sudah dibatasi komoditas koordinator. */
|
||||||
|
protected function queryPetani(): Builder
|
||||||
|
{
|
||||||
|
return $this->batasiKomoditas(Petani::query()->with(KelengkapanPetani::RELASI));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function batasiKomoditas(Builder $query): Builder
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $query->when($user->isKoordinator(), fn ($q) => $q->whereIn('komoditas_id', $user->komoditasKoordinatorIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ringkasan 3 hitungan input (skor, wajib, lainnya) per petani, dikunci id petani. */
|
||||||
|
protected function ringkasan(Collection $petani): BaseCollection
|
||||||
|
{
|
||||||
|
return $petani->mapWithKeys(fn (Petani $p) => [$p->id => KelengkapanPetani::untuk($p)->ringkasanInput()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function perPage(Request $request, string $kunci): int
|
||||||
|
{
|
||||||
|
$n = (int) $request->query($kunci);
|
||||||
|
|
||||||
|
return in_array($n, self::PER_PAGE, true) ? $n : self::PER_PAGE[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function kataCari(Request $request, string $kunci): ?string
|
||||||
|
{
|
||||||
|
return $request->filled($kunci) ? Str::lower($request->query($kunci)) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Monitoring;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Web\Concerns\AksesMonitoringPetani;
|
||||||
|
use App\Models\Pertanian\Petani;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Produktivitas\KelengkapanPetani;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu Verifikasi > Petani, tab Monitoring (sub-tab Petani & Enumerator).
|
||||||
|
*/
|
||||||
|
class MonitoringPetaniController extends Controller
|
||||||
|
{
|
||||||
|
use AksesMonitoringPetani;
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->izinkan();
|
||||||
|
|
||||||
|
// Sub-tab Petani
|
||||||
|
$petani = $this->queryPetani()
|
||||||
|
->with('creator:id,name')
|
||||||
|
->when($this->kataCari($request, 'search'), fn ($q, $cari) => $q->whereRaw('lower(nama) like ?', ["%{$cari}%"]))
|
||||||
|
->latest()
|
||||||
|
->paginate($this->perPage($request, 'per_page'))
|
||||||
|
->withQueryString()
|
||||||
|
->appends('sub', 'petani');
|
||||||
|
|
||||||
|
// Sub-tab Enumerator: jumlah petani yang diinput (created_by)
|
||||||
|
$enumerator = User::query()
|
||||||
|
->where('role', Role::ENUMERATOR)
|
||||||
|
->withCount(['petaniDibuat as jumlah_petani' => fn (Builder $q) => $this->batasiKomoditas($q)])
|
||||||
|
->when($this->kataCari($request, 'search_enumerator'), fn ($q, $cari) => $q->whereRaw('lower(name) like ?', ["%{$cari}%"]))
|
||||||
|
->orderByDesc('jumlah_petani')
|
||||||
|
->orderBy('name')
|
||||||
|
->paginate($this->perPage($request, 'per_page_enumerator'), ['*'], 'page_enumerator')
|
||||||
|
->withQueryString()
|
||||||
|
->appends('sub', 'enumerator');
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.monitoring', [
|
||||||
|
'menu' => 'petani',
|
||||||
|
'petani' => $petani,
|
||||||
|
'ringkasan' => $this->ringkasan($petani->getCollection()),
|
||||||
|
'enumerator' => $enumerator,
|
||||||
|
'perPage' => self::PER_PAGE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kartu rincian saat badge Input Skor / Wajib / Lainnya diklik (HTML, dimuat lewat fetch). */
|
||||||
|
public function detail(Petani $petani, string $jenis)
|
||||||
|
{
|
||||||
|
$this->izinkanPetani($petani);
|
||||||
|
|
||||||
|
$petani->loadMissing(['desaKelurahan.kecamatan.kabupatenKota', 'skor:id,petani_id,skor_kesiapan_adopsi']);
|
||||||
|
$kelengkapan = KelengkapanPetani::untuk($petani);
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.petani-monitoring', [
|
||||||
|
'bagian' => 'rincian',
|
||||||
|
'petani' => $petani,
|
||||||
|
'ringkasan' => $kelengkapan->ringkasanInput()[$jenis],
|
||||||
|
'rincian' => $kelengkapan->rincian($jenis)->groupBy('grup'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kartu rincian saat jumlah petani seorang enumerator diklik: daftar petani + 3 hitungan input. */
|
||||||
|
public function enumerator(User $user)
|
||||||
|
{
|
||||||
|
$this->izinkan();
|
||||||
|
abort_unless($user->role === Role::ENUMERATOR, 404);
|
||||||
|
|
||||||
|
$daftarPetani = $this->queryPetani()
|
||||||
|
->where('created_by', $user->id)
|
||||||
|
->orderBy('nama')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.enumerator', [
|
||||||
|
'bagian' => 'rincian',
|
||||||
|
'user' => $user,
|
||||||
|
'daftarPetani' => $daftarPetani,
|
||||||
|
'ringkasan' => $this->ringkasan($daftarPetani),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,10 +21,10 @@ class PetaniStatusController extends Controller
|
|||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'status' => ['required', Rule::in([StatusDataPetani::LENGKAP->value, StatusDataPetani::DRAFT->value])],
|
'status' => ['required', Rule::in([StatusDataPetani::LENGKAP->value, StatusDataPetani::DRAFT->value])],
|
||||||
// Enumerator wajib menyetujui pernyataan kelengkapan saat menandai lengkap.
|
// Enumerator menyetujui pernyataan kelengkapan (tombol konfirmasi di popup) saat menandai lengkap.
|
||||||
'pernyataan' => ['exclude_unless:status,' . StatusDataPetani::LENGKAP->value, 'accepted'],
|
'pernyataan' => ['exclude_unless:status,' . StatusDataPetani::LENGKAP->value, 'accepted'],
|
||||||
], [
|
], [
|
||||||
'pernyataan.accepted' => 'Centang pernyataan kelengkapan data terlebih dahulu.',
|
'pernyataan.accepted' => 'Setujui pernyataan kelengkapan data terlebih dahulu.',
|
||||||
]);
|
]);
|
||||||
$tujuan = StatusDataPetani::from($validated['status']);
|
$tujuan = StatusDataPetani::from($validated['status']);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Verifikasi;
|
||||||
|
|
||||||
|
use App\Enums\StatusDataPetani;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Web\Concerns\AksesMonitoringPetani;
|
||||||
|
use App\Models\Pertanian\Petani;
|
||||||
|
use App\Services\Produktivitas\KelengkapanPetani;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu Verifikasi > Petani, tab Verifikasi (sub-tab Data Lengkap & Terverifikasi).
|
||||||
|
* Koordinator/admin memeriksa data berstatus Lengkap lalu menandainya Terverifikasi.
|
||||||
|
*/
|
||||||
|
class VerifikasiPetaniController extends Controller
|
||||||
|
{
|
||||||
|
use AksesMonitoringPetani;
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->izinkan();
|
||||||
|
|
||||||
|
$cariNama = fn (string $kunci) => fn ($q) => $q->when(
|
||||||
|
$this->kataCari($request, $kunci),
|
||||||
|
fn ($q, $cari) => $q->whereRaw('lower(nama) like ?', ["%{$cari}%"])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sub-tab Data Lengkap: menunggu verifikasi
|
||||||
|
$petaniLengkap = $this->queryPetani()
|
||||||
|
->with('creator:id,name')
|
||||||
|
->where('status', StatusDataPetani::LENGKAP->value)
|
||||||
|
->tap($cariNama('search'))
|
||||||
|
->latest('updated_at')
|
||||||
|
->paginate($this->perPage($request, 'per_page'))
|
||||||
|
->withQueryString()
|
||||||
|
->appends('sub', 'lengkap');
|
||||||
|
|
||||||
|
// Sub-tab Terverifikasi
|
||||||
|
$petaniTerverifikasi = $this->batasiKomoditas(Petani::query())
|
||||||
|
->with(['creator:id,name', 'updater:id,name,role', 'komoditas:id,nama', 'desaKelurahan:id,nama'])
|
||||||
|
->where('status', StatusDataPetani::TERVERIFIKASI->value)
|
||||||
|
->tap($cariNama('search_terverifikasi'))
|
||||||
|
->latest('updated_at')
|
||||||
|
->paginate($this->perPage($request, 'per_page_terverifikasi'), ['*'], 'page_terverifikasi')
|
||||||
|
->withQueryString()
|
||||||
|
->appends('sub', 'terverifikasi');
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.verifikasi', [
|
||||||
|
'menu' => 'petani',
|
||||||
|
'petaniLengkap' => $petaniLengkap,
|
||||||
|
'ringkasan' => $this->ringkasan($petaniLengkap->getCollection()),
|
||||||
|
'petaniTerverifikasi' => $petaniTerverifikasi,
|
||||||
|
'perPage' => self::PER_PAGE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Isi kartu verifikasi (3 langkah: Input Skor → Input Wajib → Input Lainnya), dimuat lewat fetch. */
|
||||||
|
public function show(Petani $petani)
|
||||||
|
{
|
||||||
|
$this->izinkanPetani($petani);
|
||||||
|
abort_unless(in_array($petani->status, [StatusDataPetani::LENGKAP, StatusDataPetani::TERVERIFIKASI], true), 404);
|
||||||
|
|
||||||
|
$petani->loadMissing(['desaKelurahan.kecamatan.kabupatenKota', 'skor:id,petani_id,skor_kesiapan_adopsi', 'creator:id,name']);
|
||||||
|
|
||||||
|
// Sudah terverifikasi: kartu ringkas (siapa & kapan diverifikasi) + Kembalikan ke Draf.
|
||||||
|
if ($petani->status === StatusDataPetani::TERVERIFIKASI) {
|
||||||
|
$petani->loadMissing(['komoditas:id,nama', 'updater:id,name,role']);
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.petani-terverifikasi', ['bagian' => 'rincian', 'petani' => $petani]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$kelengkapan = KelengkapanPetani::untuk($petani);
|
||||||
|
|
||||||
|
return view('monitoring-verifikasi.petani-lengkap', [
|
||||||
|
'bagian' => 'rincian',
|
||||||
|
'petani' => $petani,
|
||||||
|
'ringkasan' => $kelengkapan->ringkasanInput(),
|
||||||
|
'rincian' => collect(['skor', 'wajib', 'lainnya'])
|
||||||
|
->mapWithKeys(fn (string $jenis) => [$jenis => $kelengkapan->rincian($jenis)->groupBy('grup')]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tandai Terverifikasi (dari langkah terakhir kartu verifikasi). */
|
||||||
|
public function update(Petani $petani)
|
||||||
|
{
|
||||||
|
$this->izinkanPetani($petani);
|
||||||
|
|
||||||
|
$kembali = redirect()->route('monitoring-verifikasi.petani.verifikasi', ['sub' => 'lengkap']);
|
||||||
|
|
||||||
|
if ($petani->status !== StatusDataPetani::LENGKAP) {
|
||||||
|
return $kembali->with('verifikasi_error', "Data petani {$petani->nama} tidak berstatus Lengkap, tidak dapat diverifikasi.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$petani->status = StatusDataPetani::TERVERIFIKASI;
|
||||||
|
$petani->updated_by = auth()->id();
|
||||||
|
$petani->save();
|
||||||
|
|
||||||
|
return $kembali->with('verifikasi_success', "Data petani {$petani->nama} berhasil diverifikasi.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kembalikan ke Draf dengan catatan untuk enumerator:
|
||||||
|
* - dari Lengkap → tombol "Belum Lengkap" di kartu verifikasi
|
||||||
|
* - dari Terverifikasi → ada temuan setelah diverifikasi
|
||||||
|
*/
|
||||||
|
public function kembalikanKeDraf(Request $request, Petani $petani)
|
||||||
|
{
|
||||||
|
$this->izinkanPetani($petani);
|
||||||
|
|
||||||
|
$data = $request->validate(
|
||||||
|
['catatan' => ['required', 'string', 'max:2000']],
|
||||||
|
['catatan.required' => 'Catatan wajib diisi.', 'catatan.max' => 'Catatan maksimal 2000 karakter.']
|
||||||
|
);
|
||||||
|
|
||||||
|
$dariTerverifikasi = $petani->status === StatusDataPetani::TERVERIFIKASI;
|
||||||
|
$kembali = redirect()->route('monitoring-verifikasi.petani.verifikasi', ['sub' => $dariTerverifikasi ? 'terverifikasi' : 'lengkap']);
|
||||||
|
|
||||||
|
if (! $dariTerverifikasi && $petani->status !== StatusDataPetani::LENGKAP) {
|
||||||
|
return $kembali->with('verifikasi_error', "Data petani {$petani->nama} sudah berstatus Draf.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$petani->status = StatusDataPetani::DRAFT;
|
||||||
|
$petani->catatan = trim($data['catatan']);
|
||||||
|
$petani->updated_by = auth()->id();
|
||||||
|
$petani->save();
|
||||||
|
|
||||||
|
return $kembali->with('verifikasi_success', "Data petani {$petani->nama} dikembalikan ke Draf dengan catatan untuk enumerator.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
|
use App\Enums\StatusDataPetani;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -12,6 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
* Dipasang di semua route modul data petani (petani/{petani}/...).
|
* Dipasang di semua route modul data petani (petani/{petani}/...).
|
||||||
* - Hanya role pengelola data petani yang boleh masuk.
|
* - Hanya role pengelola data petani yang boleh masuk.
|
||||||
* - Koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan.
|
* - Koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan.
|
||||||
|
* - Petani berstatus Terverifikasi hanya bisa dilihat (simpan/hapus ditolak).
|
||||||
*/
|
*/
|
||||||
class BatasiAksesPetani
|
class BatasiAksesPetani
|
||||||
{
|
{
|
||||||
@@ -30,9 +32,18 @@ class BatasiAksesPetani
|
|||||||
|
|
||||||
$petani = $request->route('petani');
|
$petani = $request->route('petani');
|
||||||
|
|
||||||
if ($petani !== null && $user->isKoordinator()) {
|
// Data terverifikasi dikunci: tidak bisa diubah, ditambah, atau dihapus (hanya dilihat).
|
||||||
|
// Perubahan status ditangani sendiri oleh PetaniStatusController.
|
||||||
|
$permintaanUbah = ! $request->isMethodSafe() && ! $request->routeIs('petani.status.*');
|
||||||
|
|
||||||
|
// Petani hanya dimuat kalau perlu dicek (koordinator, atau permintaan simpan/hapus).
|
||||||
|
if ($petani === null || (! $user->isKoordinator() && ! $permintaanUbah)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
$petani = $petani instanceof Petani ? $petani : Petani::find($petani);
|
$petani = $petani instanceof Petani ? $petani : Petani::find($petani);
|
||||||
|
|
||||||
|
if ($user->isKoordinator()) {
|
||||||
abort_unless(
|
abort_unless(
|
||||||
$petani && $user->bolehAksesPetani($petani),
|
$petani && $user->bolehAksesPetani($petani),
|
||||||
403,
|
403,
|
||||||
@@ -40,6 +51,16 @@ class BatasiAksesPetani
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($permintaanUbah && $petani?->status === StatusDataPetani::TERVERIFIKASI) {
|
||||||
|
$pesan = 'Data petani sudah terverifikasi dan tidak dapat diubah atau dihapus.';
|
||||||
|
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
abort(403, $pesan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('status_petani_error', $pesan);
|
||||||
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ use Closure;
|
|||||||
*/
|
*/
|
||||||
trait KembalikanStatusPetaniKeDraf
|
trait KembalikanStatusPetaniKeDraf
|
||||||
{
|
{
|
||||||
// Kolom otomatis/audit yang tidak dihitung sebagai perubahan data.
|
// Kolom otomatis/audit (dan catatan koordinator) yang tidak dihitung sebagai perubahan data.
|
||||||
private static array $kolomBukanPerubahanData = [
|
private static array $kolomBukanPerubahanData = [
|
||||||
'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'deleted_by', 'status',
|
'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'deleted_by', 'status', 'catatan',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function bootKembalikanStatusPetaniKeDraf(): void
|
public static function bootKembalikanStatusPetaniKeDraf(): void
|
||||||
|
|||||||
@@ -422,6 +422,11 @@ class Petani extends Model
|
|||||||
return $this->belongsTo(User::class, 'created_by');
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
// Untuk trait KembalikanStatusPetaniKeDraf: petani ini sendiri.
|
// Untuk trait KembalikanStatusPetaniKeDraf: petani ini sendiri.
|
||||||
protected function petaniIdUntukStatus(): ?int
|
protected function petaniIdUntukStatus(): ?int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,12 +3,24 @@
|
|||||||
namespace App\Services\Produktivitas;
|
namespace App\Services\Produktivitas;
|
||||||
|
|
||||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||||
|
use App\Enums\AksesPasar\SkorKesadaranPremium;
|
||||||
|
use App\Enums\AksesPasar\SkorPengalaman;
|
||||||
|
use App\Enums\AksesPasar\TingkatKepercayaanPembeli;
|
||||||
|
use App\Enums\PraktikBudidaya\DasarPenentuanPanen;
|
||||||
|
use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama;
|
||||||
|
use App\Enums\PraktikBudidaya\FrekuensiSanitasiLahan;
|
||||||
|
use App\Enums\PraktikBudidaya\KondisiPengelolaanAir;
|
||||||
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
|
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
|
||||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||||
|
use App\Enums\PraktikBudidaya\PenangananGulma;
|
||||||
|
use App\Enums\PraktikBudidaya\PraktikSortasi;
|
||||||
|
use App\Enums\PraktikBudidaya\TingkatPanenReject;
|
||||||
use App\Enums\SosialEkonomi\TerdaftarKelompokTani;
|
use App\Enums\SosialEkonomi\TerdaftarKelompokTani;
|
||||||
use App\Models\Pertanian\Lahan;
|
use App\Models\Pertanian\Lahan;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
|
use Closure;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Daftar isian Profil Petani yang WAJIB (dipakai skor Kesiapan Adopsi)
|
* Daftar isian Profil Petani yang WAJIB (dipakai skor Kesiapan Adopsi)
|
||||||
@@ -113,18 +125,66 @@ class KelengkapanPetani
|
|||||||
];
|
];
|
||||||
private const ISIAN_HARGA_JUAL = ['bentuk_produk', 'harga_panen_terakhir', 'harga_tahun_lalu', 'harga_dua_tahun_lalu'];
|
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.
|
* Kolom yang sumber isian skor / dashboard (lihat profil(), lahan(), ..., pertanyaanSkor()).
|
||||||
* WeakMap: otomatis dibuang saat objek Petani tidak dipakai lagi (tidak menumpuk antar-request).
|
* Kolom di luar daftar ini dihitung sebagai "isian lainnya".
|
||||||
*/
|
*/
|
||||||
private static ?\WeakMap $cache = null;
|
private const PRIORITAS_PROFIL = ['jenis_kelamin', 'tanggal_lahir', 'pendidikan_terakhir', 'komoditas_id', 'desa_kelurahan_id'];
|
||||||
|
private const PRIORITAS_LAHAN = [
|
||||||
|
'desa_kelurahan_id', 'path', 'luas_lahan', 'jumlah_tanaman', 'kondisi_tanaman', 'keparahan_serangan',
|
||||||
|
'jenis_hama', 'jenis_penyakit', 'umur_tanaman',
|
||||||
|
];
|
||||||
|
private const PRIORITAS_PRAKTIK_BUDIDAYA = [
|
||||||
|
'frekuensi_pemangkasan_utama', 'pemberian_pupuk', 'metode_penanganan_opt', 'jenis_hama', 'jenis_penyakit',
|
||||||
|
'frekuensi_sanitasi_lahan', 'penanganan_gulma', 'kondisi_pengelolaan_air', 'sumber_air',
|
||||||
|
'kebiasaan_konservasi_tanah', 'pemahaman_manfaat_konservasi_tanah', 'total_estimasi_biaya_produksi',
|
||||||
|
];
|
||||||
|
private const PRIORITAS_PANEN = ['dasar_penentuan_panen', 'hasil_komoditas_utama_permusim', 'praktik_sortasi', 'hasil_panen_reject'];
|
||||||
|
private const PRIORITAS_SOSIAL_EKONOMI = [
|
||||||
|
'jumlah_tanggungan', 'skor_periode_paceklik', 'skor_kemampuan_menabung', 'skor_kecukupan_penghasilan',
|
||||||
|
'skor_kejelasan_status_lahan', 'kemauan_mencoba_praktik_baru', 'pemahaman_mutu_harga', 'panutan_petani_sukses',
|
||||||
|
'kemauan_ikut_program', 'orientasi_rencana_jangka_panjang', 'terdaftar_kelompok_tani',
|
||||||
|
'keaktifan_pertemuan_kelompok', 'peran_dalam_kelompok', 'manfaat_kelompok_tani', 'status_rekening_tabungan',
|
||||||
|
'pengalaman_kredit_kebun', 'kondisi_aset_produktif', 'keterlibatan_program_keuangan_formal',
|
||||||
|
'keterikatan_hutang_panen', 'praktik_ijon', 'kebebasan_memilih_pembeli',
|
||||||
|
];
|
||||||
|
private const PRIORITAS_PENDAPATAN_KOMODITAS = ['pendapatan'];
|
||||||
|
private const PRIORITAS_AKSES_PASAR = [
|
||||||
|
'tujuan_penjualan', 'sifat_hubungan_bisnis', 'frekuensi_penjualan', 'skor_pengalaman', 'praktik_fermentasi',
|
||||||
|
'skor_kesadaran_premium', 'tingkat_kepercayaan_pembeli_baru',
|
||||||
|
];
|
||||||
|
|
||||||
private ?Collection $semuaKosong = null;
|
/** Kolom angka tanpa cast enum di model -> enum untuk teks jawabannya. */
|
||||||
private ?array $hitungIsian = null;
|
private const ENUM_JAWABAN = [
|
||||||
|
'frekuensi_pemangkasan_utama' => FrekuensiPemangkasanUtama::class,
|
||||||
|
'pemberian_pupuk' => PemberianPupuk::class,
|
||||||
|
'metode_penanganan_opt' => MetodePenangananHamaPenyakit::class,
|
||||||
|
'frekuensi_sanitasi_lahan' => FrekuensiSanitasiLahan::class,
|
||||||
|
'penanganan_gulma' => PenangananGulma::class,
|
||||||
|
'kondisi_pengelolaan_air' => KondisiPengelolaanAir::class,
|
||||||
|
'dasar_penentuan_panen' => DasarPenentuanPanen::class,
|
||||||
|
'praktik_sortasi' => PraktikSortasi::class,
|
||||||
|
'hasil_panen_reject' => TingkatPanenReject::class,
|
||||||
|
'skor_pengalaman' => SkorPengalaman::class,
|
||||||
|
'skor_kesadaran_premium' => SkorKesadaranPremium::class,
|
||||||
|
'tingkat_kepercayaan_pembeli_baru' => TingkatKepercayaanPembeli::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(private Petani $petani)
|
/** Kolom berkas/peta: cukup ditampilkan "Sudah diunggah". */
|
||||||
{
|
private const KOLOM_BERKAS = ['foto', 'path', 'path_kk', 'path_ktp'];
|
||||||
$petani->loadMissing([
|
|
||||||
|
/** Label isian lainnya yang tidak cukup dibaca dari nama kolom. */
|
||||||
|
private const LABEL_KOLOM = [
|
||||||
|
'ktp' => 'NIK', 'kk' => 'No. KK', 'path_kk' => 'Berkas KK', 'path_ktp' => 'Berkas KTP',
|
||||||
|
'path' => 'Peta/polygon lahan', 'komoditas_id' => 'Komoditas', 'jenis_komoditas_id' => 'Jenis komoditas',
|
||||||
|
'varietas_id' => 'Varietas', 'desa_kelurahan_id' => 'Desa/kelurahan', 'is_aktif' => 'Sertifikat masih aktif',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Jumlah pertanyaan yang dinilai di skor Kesiapan Adopsi (lihat pertanyaanSkor()). */
|
||||||
|
public const JUMLAH_PERTANYAAN_SKOR = 32;
|
||||||
|
|
||||||
|
/** Relasi yang dibaca; eager-load ini di daftar petani supaya tidak N+1. */
|
||||||
|
public const RELASI = [
|
||||||
'komoditas',
|
'komoditas',
|
||||||
'lahans',
|
'lahans',
|
||||||
'praktikBudidaya',
|
'praktikBudidaya',
|
||||||
@@ -136,7 +196,29 @@ class KelengkapanPetani
|
|||||||
'sosialEkonomi.pendapatanLuarPertanian',
|
'sosialEkonomi.pendapatanLuarPertanian',
|
||||||
'aksesPasar',
|
'aksesPasar',
|
||||||
'hargaJualPanen',
|
'hargaJualPanen',
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// Hasil hitung disimpan per instance (header, tabel, & kartu rincian memakai instance yang sama).
|
||||||
|
private ?Collection $semuaItem = null;
|
||||||
|
private ?Collection $semuaKosong = null;
|
||||||
|
private ?Collection $isianDashboard = null;
|
||||||
|
private ?array $hitungIsian = null;
|
||||||
|
private ?array $pertanyaanSkor = null;
|
||||||
|
private ?array $ringkasanInput = null;
|
||||||
|
|
||||||
|
/** Diisi hanya saat rincianLainnya() dipanggil (side panel), supaya daftar petani tetap ringan. */
|
||||||
|
private ?array $rincianLainnya = null;
|
||||||
|
private string $grupRincian = '';
|
||||||
|
|
||||||
|
public function __construct(private Petani $petani)
|
||||||
|
{
|
||||||
|
$petani->loadMissing(self::RELASI);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function untuk(Petani $petani): self
|
public static function untuk(Petani $petani): self
|
||||||
@@ -161,19 +243,256 @@ class KelengkapanPetani
|
|||||||
return $this->hitungIsian ??= $this->hitungSemuaIsian();
|
return $this->hitungIsian ??= $this->hitungSemuaIsian();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ringkasan untuk tabel Monitoring: masing-masing ['terisi' => int, 'total' => int].
|
||||||
|
* - skor: 32 pertanyaan skor Kesiapan Adopsi
|
||||||
|
* - wajib: isian yang dipakai dashboard (prioritas PERLU)
|
||||||
|
* - lainnya: isian lain di luar keduanya (yang tidak berlaku tidak dihitung)
|
||||||
|
*/
|
||||||
|
public function ringkasanInput(): array
|
||||||
|
{
|
||||||
|
if ($this->ringkasanInput !== null) {
|
||||||
|
return $this->ringkasanInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
$skor = collect($this->pertanyaanSkor());
|
||||||
|
$dashboard = $this->isianDashboard();
|
||||||
|
$lainnya = $this->hitungIsian()['lainnya'];
|
||||||
|
|
||||||
|
return $this->ringkasanInput = [
|
||||||
|
'skor' => ['terisi' => $skor->where('terisi', true)->count(), 'total' => $skor->count()],
|
||||||
|
'wajib' => ['terisi' => $dashboard->where('terisi', true)->count(), 'total' => $dashboard->count()],
|
||||||
|
'lainnya' => ['terisi' => $lainnya['terisi'], 'total' => $lainnya['total'] - $lainnya['tidak_berlaku']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Warna badge hitungan ['terisi','total']: hijau lengkap, kuning sebagian, merah kosong. */
|
||||||
|
public static function warna(array $hitungan): string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
$hitungan['total'] > 0 && $hitungan['terisi'] >= $hitungan['total'] => 'success',
|
||||||
|
$hitungan['terisi'] > 0 => 'warning',
|
||||||
|
default => 'danger',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rincian pertanyaan → jawaban untuk side panel Monitoring.
|
||||||
|
* Setiap baris: grup, label, jawaban (string|null = belum diisi), terisi.
|
||||||
|
*
|
||||||
|
* @param 'skor'|'wajib'|'lainnya' $jenis
|
||||||
|
*/
|
||||||
|
public function rincian(string $jenis): Collection
|
||||||
|
{
|
||||||
|
return match ($jenis) {
|
||||||
|
'skor' => collect($this->pertanyaanSkor()),
|
||||||
|
'wajib' => $this->isianDashboard()->map(fn (array $item) => [
|
||||||
|
'grup' => $item['grup'] ?? self::TAB_LABEL[$item['tab']],
|
||||||
|
'label' => $item['label'],
|
||||||
|
'jawaban' => isset($item['jawaban']) ? ($item['jawaban'])() : self::jawaban($item['nilai'], $item['input']),
|
||||||
|
'terisi' => $item['terisi'],
|
||||||
|
]),
|
||||||
|
'lainnya' => $this->rincianLainnya(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Isian PERLU (dashboard) dari semua tab, tiap item diberi kunci 'tab'. */
|
||||||
|
private function isianDashboard(): Collection
|
||||||
|
{
|
||||||
|
return $this->isianDashboard ??= $this->semuaItem()
|
||||||
|
->flatMap(fn (array $items, string $tab) => array_map(fn ($item) => [...$item, 'tab' => $tab], $items))
|
||||||
|
->where('prioritas', self::PERLU)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 32 pertanyaan skor (petani_skor): sudah bisa dinilai atau belum, beserta jawabannya.
|
||||||
|
* Pertanyaan lanjutan yang tidak berlaku (mis. data kelompok kalau bukan anggota) dianggap sudah terjawab.
|
||||||
|
*/
|
||||||
|
private function pertanyaanSkor(): array
|
||||||
|
{
|
||||||
|
return $this->pertanyaanSkor ??= $this->susunPertanyaanSkor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function susunPertanyaanSkor(): array
|
||||||
|
{
|
||||||
|
$p = $this->petani;
|
||||||
|
$pb = $p->praktikBudidaya;
|
||||||
|
$panen = $p->surveyPanen;
|
||||||
|
$se = $p->sosialEkonomi;
|
||||||
|
$ap = $p->aksesPasar;
|
||||||
|
|
||||||
|
$tanya = fn (string $grup, string $label, ?object $model, string $kolom) => [
|
||||||
|
'grup' => $grup,
|
||||||
|
'label' => $label,
|
||||||
|
'jawaban' => self::jawaban($model?->{$kolom}, $kolom),
|
||||||
|
'terisi' => $model && self::terisi($model->{$kolom}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Data kelompok hanya ditanyakan ke anggota.
|
||||||
|
$terdaftar = self::nilaiEnum($se?->terdaftar_kelompok_tani);
|
||||||
|
$bukanAnggota = self::terisi($terdaftar) && $terdaftar !== TerdaftarKelompokTani::YA->value;
|
||||||
|
$kelompok = fn (string $label, string $kolom) => $bukanAnggota
|
||||||
|
? ['grup' => 'Sosial Ekonomi', 'label' => $label, 'jawaban' => 'Tidak berlaku (bukan anggota kelompok tani)', 'terisi' => true]
|
||||||
|
: [...$tanya('Sosial Ekonomi', $label, $se, $kolom), 'terisi' => self::terisi($terdaftar) && $se && self::terisi($se->{$kolom})];
|
||||||
|
|
||||||
|
// Konservasi: pemahaman manfaat hanya ditanyakan kalau ada praktik konservasi.
|
||||||
|
$konservasi = (array) ($pb?->kebiasaan_konservasi_tanah ?? []);
|
||||||
|
$adaKonservasi = collect($konservasi)->contains(fn ($v) => $v !== self::KONSERVASI_TIDAK_ADA);
|
||||||
|
|
||||||
|
// Rasio biaya produksi : pendapatan.
|
||||||
|
$biaya = $pb?->total_estimasi_biaya_produksi;
|
||||||
|
$pendapatan = (int) ($se?->pendapatanKomoditas->sum('pendapatan') ?? 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Sosial Ekonomi (20)
|
||||||
|
$tanya('Sosial Ekonomi', 'Kemauan mencoba praktik baru', $se, 'kemauan_mencoba_praktik_baru'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Pemahaman mutu & harga', $se, 'pemahaman_mutu_harga'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Panutan petani sukses', $se, 'panutan_petani_sukses'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Kemauan ikut program', $se, 'kemauan_ikut_program'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Orientasi rencana jangka panjang', $se, 'orientasi_rencana_jangka_panjang'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Periode paceklik', $se, 'skor_periode_paceklik'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Kemampuan menabung', $se, 'skor_kemampuan_menabung'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Kecukupan penghasilan', $se, 'skor_kecukupan_penghasilan'),
|
||||||
|
[
|
||||||
|
'grup' => 'Sosial Ekonomi',
|
||||||
|
'label' => 'Rasio biaya produksi terhadap pendapatan',
|
||||||
|
'jawaban' => self::terisi($biaya) || $pendapatan > 0
|
||||||
|
? 'Biaya ' . self::rupiah($biaya) . ' / Pendapatan ' . self::rupiah($pendapatan)
|
||||||
|
: null,
|
||||||
|
'terisi' => self::terisi($biaya) && $pendapatan > 0,
|
||||||
|
],
|
||||||
|
$tanya('Sosial Ekonomi', 'Kejelasan status lahan', $se, 'skor_kejelasan_status_lahan'),
|
||||||
|
$kelompok('Keaktifan pertemuan kelompok', 'keaktifan_pertemuan_kelompok'),
|
||||||
|
$kelompok('Peran dalam kelompok', 'peran_dalam_kelompok'),
|
||||||
|
$kelompok('Manfaat kelompok tani', 'manfaat_kelompok_tani'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Status rekening tabungan', $se, 'status_rekening_tabungan'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Pengalaman kredit kebun', $se, 'pengalaman_kredit_kebun'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Kondisi aset produktif', $se, 'kondisi_aset_produktif'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Keterlibatan program keuangan formal', $se, 'keterlibatan_program_keuangan_formal'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Keterikatan hutang panen', $se, 'keterikatan_hutang_panen'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Praktik ijon', $se, 'praktik_ijon'),
|
||||||
|
$tanya('Sosial Ekonomi', 'Kebebasan memilih pembeli', $se, 'kebebasan_memilih_pembeli'),
|
||||||
|
|
||||||
|
// Praktik Budidaya (7)
|
||||||
|
$tanya('Praktik Budidaya', 'Frekuensi pemangkasan', $pb, 'frekuensi_pemangkasan_utama'),
|
||||||
|
$tanya('Praktik Budidaya', 'Pemberian pupuk', $pb, 'pemberian_pupuk'),
|
||||||
|
$tanya('Praktik Budidaya', 'Metode penanganan hama & penyakit', $pb, 'metode_penanganan_opt'),
|
||||||
|
$tanya('Praktik Budidaya', 'Frekuensi sanitasi lahan', $pb, 'frekuensi_sanitasi_lahan'),
|
||||||
|
$tanya('Praktik Budidaya', 'Penanganan gulma', $pb, 'penanganan_gulma'),
|
||||||
|
$tanya('Praktik Budidaya', 'Kondisi pengelolaan air & irigasi', $pb, 'kondisi_pengelolaan_air'),
|
||||||
|
[
|
||||||
|
'grup' => 'Praktik Budidaya',
|
||||||
|
'label' => 'Praktik konservasi tanah',
|
||||||
|
'jawaban' => self::jawaban(array_filter([
|
||||||
|
self::jawaban($konservasi),
|
||||||
|
$adaKonservasi ? self::jawaban($pb?->pemahaman_manfaat_konservasi_tanah) : null,
|
||||||
|
]), null, ' · '),
|
||||||
|
'terisi' => self::terisi($konservasi) && (! $adaKonservasi || self::terisi($pb?->pemahaman_manfaat_konservasi_tanah)),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Panen (2)
|
||||||
|
$tanya('Panen', 'Dasar penentuan panen', $panen, 'dasar_penentuan_panen'),
|
||||||
|
$tanya('Panen', 'Praktik sortasi', $panen, 'praktik_sortasi'),
|
||||||
|
|
||||||
|
// Akses Pasar (3); hambatan mutu dari tingkat hasil panen ditolak pembeli
|
||||||
|
$tanya('Akses Pasar', 'Pengalaman menjual ke saluran lain', $ap, 'skor_pengalaman'),
|
||||||
|
$tanya('Akses Pasar', 'Kesadaran pasar premium', $ap, 'skor_kesadaran_premium'),
|
||||||
|
$tanya('Akses Pasar', 'Hambatan mutu (hasil panen ditolak pembeli)', $panen, 'hasil_panen_reject'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ubah nilai isian jadi teks jawaban. null = belum diisi.
|
||||||
|
* Kolom yang disimpan sebagai angka (tanpa cast enum di model) diterjemahkan lewat ENUM_JAWABAN.
|
||||||
|
*/
|
||||||
|
public static function jawaban(mixed $nilai, ?string $kolom = null, string $pemisah = ', '): ?string
|
||||||
|
{
|
||||||
|
if (! self::terisi($nilai)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($kolom && in_array($kolom, self::KOLOM_BERKAS, true)) {
|
||||||
|
return 'Sudah diunggah';
|
||||||
|
}
|
||||||
|
if ($nilai instanceof Collection) {
|
||||||
|
$nilai = $nilai->all();
|
||||||
|
}
|
||||||
|
if (is_array($nilai)) {
|
||||||
|
$isi = array_filter(array_map(fn ($v) => self::jawaban($v, $kolom), $nilai));
|
||||||
|
|
||||||
|
return $isi ? implode($pemisah, $isi) : null;
|
||||||
|
}
|
||||||
|
if ($kolom && isset(self::ENUM_JAWABAN[$kolom]) && is_numeric($nilai)) {
|
||||||
|
$nilai = self::ENUM_JAWABAN[$kolom]::tryFrom((int) $nilai) ?? $nilai;
|
||||||
|
}
|
||||||
|
// Kolom uang (biaya, upah, harga, hutang, tabungan, pendapatan) → "Rp 1.500.000".
|
||||||
|
if ($kolom && is_numeric($nilai) && preg_match('/biaya|upah|harga|hutang_rupiah|tabungan|pendapatan|estimasi_per_bulan/', $kolom)) {
|
||||||
|
return self::rupiah($nilai);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
$nilai instanceof \BackedEnum => self::labelEnum($nilai),
|
||||||
|
$nilai instanceof \DateTimeInterface => $nilai->format('d-m-Y'),
|
||||||
|
is_bool($nilai) => $nilai ? 'Ya' : 'Tidak',
|
||||||
|
is_object($nilai) => json_encode($nilai),
|
||||||
|
default => (string) $nilai,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Label enum: dari label(), atau dari options() untuk enum yang hanya punya daftar opsi. */
|
||||||
|
private static function labelEnum(\BackedEnum $enum): string
|
||||||
|
{
|
||||||
|
if (method_exists($enum, 'label')) {
|
||||||
|
return $enum->label();
|
||||||
|
}
|
||||||
|
|
||||||
|
$opsi = method_exists($enum, 'options') ? collect($enum::options())->firstWhere('value', $enum->value) : null;
|
||||||
|
|
||||||
|
return $opsi['label'] ?? (string) $enum->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function rupiah(mixed $nilai): string
|
||||||
|
{
|
||||||
|
return self::terisi($nilai) ? 'Rp ' . number_format((float) $nilai, 0, ',', '.') : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "Biji kering: Rp 65.000, Fermentasi: Rp 80.000" dari baris harga jual yang terisi. */
|
||||||
|
private static function jawabanHarga(Collection $harga, string $kolom): ?string
|
||||||
|
{
|
||||||
|
return self::jawaban($harga->filter(fn ($h) => (float) $h->{$kolom} > 0)
|
||||||
|
->map(fn ($h) => trim(($h->bentuk_produk ? $h->bentuk_produk . ': ' : '') . self::rupiah($h->{$kolom})))
|
||||||
|
->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Isian lainnya (di luar skor & dashboard) beserta jawabannya; tidak termasuk yang tidak berlaku. */
|
||||||
|
private function rincianLainnya(): Collection
|
||||||
|
{
|
||||||
|
$this->rincianLainnya = [];
|
||||||
|
$this->hitungSemuaIsian();
|
||||||
|
$hasil = collect($this->rincianLainnya)->map(fn (array $r) => [...$r, 'jawaban' => ($r['jawaban'])()]);
|
||||||
|
$this->rincianLainnya = null;
|
||||||
|
|
||||||
|
return $hasil;
|
||||||
|
}
|
||||||
|
|
||||||
private function hitungSemuaIsian(): array
|
private function hitungSemuaIsian(): array
|
||||||
{
|
{
|
||||||
$hasil = ['terisi' => 0, 'total' => 0, 'tidak_berlaku' => 0];
|
$hasil = [
|
||||||
|
'terisi' => 0, 'total' => 0, 'tidak_berlaku' => 0,
|
||||||
|
'lainnya' => ['terisi' => 0, 'total' => 0, 'tidak_berlaku' => 0],
|
||||||
|
];
|
||||||
$p = $this->petani;
|
$p = $this->petani;
|
||||||
|
|
||||||
// Profil (+ provinsi/kabupaten/kecamatan yang mengikuti desa)
|
// Profil (+ provinsi/kabupaten/kecamatan yang mengikuti desa; termasuk prioritas)
|
||||||
$this->hitungKolom($hasil, $p, self::ISIAN_PROFIL);
|
$this->grupRincian = 'Profil';
|
||||||
|
$this->hitungKolom($hasil, $p, self::ISIAN_PROFIL, [], self::PRIORITAS_PROFIL);
|
||||||
$this->hitungNilai($hasil, 3, self::terisi($p->desa_kelurahan_id));
|
$this->hitungNilai($hasil, 3, self::terisi($p->desa_kelurahan_id));
|
||||||
|
|
||||||
// Lahan & Tanaman, per lahan. Belum ada lahan = dihitung 1 lahan kosong.
|
// Lahan & Tanaman, per lahan. Belum ada lahan = dihitung 1 lahan kosong.
|
||||||
$daftarLahan = $p->lahans->isEmpty() ? collect([null]) : $p->lahans;
|
$daftarLahan = $p->lahans->isEmpty() ? collect([null]) : $p->lahans;
|
||||||
foreach ($daftarLahan as $lahan) {
|
foreach ($daftarLahan->values() as $i => $lahan) {
|
||||||
$this->hitungKolom($hasil, $lahan, self::ISIAN_LAHAN);
|
$this->grupRincian = $lahan?->nama ?: 'Lahan ' . ($i + 1);
|
||||||
|
$this->hitungKolom($hasil, $lahan, self::ISIAN_LAHAN, [], self::PRIORITAS_LAHAN);
|
||||||
$this->hitungNilai($hasil, 3, self::terisi($lahan?->desa_kelurahan_id));
|
$this->hitungNilai($hasil, 3, self::terisi($lahan?->desa_kelurahan_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +509,14 @@ class KelengkapanPetani
|
|||||||
if ((int) $pb?->metode_penanganan_opt === MetodePenangananHamaPenyakit::TIDAK_ADA->value) {
|
if ((int) $pb?->metode_penanganan_opt === MetodePenangananHamaPenyakit::TIDAK_ADA->value) {
|
||||||
array_push($tidakBerlakuPb, 'jenis_hama', 'jenis_penyakit');
|
array_push($tidakBerlakuPb, 'jenis_hama', 'jenis_penyakit');
|
||||||
}
|
}
|
||||||
$this->hitungKolom($hasil, $pb, self::ISIAN_PRAKTIK_BUDIDAYA, $tidakBerlakuPb);
|
$this->grupRincian = 'Praktik Budidaya';
|
||||||
$this->hitungKolom($hasil, $p->surveyPanen, self::ISIAN_PANEN);
|
$this->hitungKolom($hasil, $pb, self::ISIAN_PRAKTIK_BUDIDAYA, $tidakBerlakuPb, self::PRIORITAS_PRAKTIK_BUDIDAYA);
|
||||||
|
$this->grupRincian = 'Panen & Produktivitas';
|
||||||
|
$this->hitungKolom($hasil, $p->surveyPanen, self::ISIAN_PANEN, [], self::PRIORITAS_PANEN);
|
||||||
$this->hitungRepeater($hasil, $p->surveyProduktivitas, self::ISIAN_PRODUKTIVITAS);
|
$this->hitungRepeater($hasil, $p->surveyProduktivitas, self::ISIAN_PRODUKTIVITAS);
|
||||||
|
|
||||||
// Pendidikan & Pelatihan
|
// Pendidikan & Pelatihan
|
||||||
|
$this->grupRincian = 'Pendidikan & Pelatihan';
|
||||||
$this->hitungRepeater($hasil, $p->surveyPelatihan, self::ISIAN_PELATIHAN);
|
$this->hitungRepeater($hasil, $p->surveyPelatihan, self::ISIAN_PELATIHAN);
|
||||||
$this->hitungRepeater($hasil, $p->surveySertifikasi, self::ISIAN_SERTIFIKASI);
|
$this->hitungRepeater($hasil, $p->surveySertifikasi, self::ISIAN_SERTIFIKASI);
|
||||||
|
|
||||||
@@ -204,9 +526,10 @@ class KelengkapanPetani
|
|||||||
? ['nama_kelompok_tani', 'lama_bergabung_kelompok_tahun', 'kedekatan_sesama_anggota',
|
? ['nama_kelompok_tani', 'lama_bergabung_kelompok_tahun', 'kedekatan_sesama_anggota',
|
||||||
'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok', 'manfaat_kelompok_tani']
|
'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok', 'manfaat_kelompok_tani']
|
||||||
: [];
|
: [];
|
||||||
$this->hitungKolom($hasil, $se, self::ISIAN_SOSIAL_EKONOMI, $tidakBerlakuSe);
|
$this->grupRincian = 'Sosial Ekonomi';
|
||||||
|
$this->hitungKolom($hasil, $se, self::ISIAN_SOSIAL_EKONOMI, $tidakBerlakuSe, self::PRIORITAS_SOSIAL_EKONOMI);
|
||||||
$this->hitungRepeater($hasil, collect($se?->detail_bantuan_ngo_perusahaan ?? []), self::ISIAN_BANTUAN_NGO);
|
$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?->pendapatanKomoditas ?? collect(), self::ISIAN_PENDAPATAN_KOMODITAS, self::PRIORITAS_PENDAPATAN_KOMODITAS);
|
||||||
$this->hitungRepeater($hasil, $se?->pendapatanLuarPertanian ?? collect(), self::ISIAN_PENDAPATAN_LUAR);
|
$this->hitungRepeater($hasil, $se?->pendapatanLuarPertanian ?? collect(), self::ISIAN_PENDAPATAN_LUAR);
|
||||||
|
|
||||||
// Akses Pasar (detail fermentasi tidak berlaku kalau belum pernah)
|
// Akses Pasar (detail fermentasi tidak berlaku kalau belum pernah)
|
||||||
@@ -214,8 +537,9 @@ class KelengkapanPetani
|
|||||||
$tidakBerlakuAp = self::nilaiEnum($ap?->praktik_fermentasi) === PraktikFermentasi::BELUM_PERNAH->value
|
$tidakBerlakuAp = self::nilaiEnum($ap?->praktik_fermentasi) === PraktikFermentasi::BELUM_PERNAH->value
|
||||||
? ['proses_fermentasi', 'model_fermentasi_memungkinkan']
|
? ['proses_fermentasi', 'model_fermentasi_memungkinkan']
|
||||||
: [];
|
: [];
|
||||||
$this->hitungKolom($hasil, $ap, self::ISIAN_AKSES_PASAR, $tidakBerlakuAp);
|
$this->grupRincian = 'Akses Pasar';
|
||||||
$this->hitungRepeater($hasil, $p->hargaJualPanen, self::ISIAN_HARGA_JUAL);
|
$this->hitungKolom($hasil, $ap, self::ISIAN_AKSES_PASAR, $tidakBerlakuAp, self::PRIORITAS_AKSES_PASAR);
|
||||||
|
$this->hitungRepeater($hasil, $p->hargaJualPanen, self::ISIAN_HARGA_JUAL, self::ISIAN_HARGA_JUAL);
|
||||||
|
|
||||||
$hasil['berlaku'] = $hasil['total'] - $hasil['tidak_berlaku'];
|
$hasil['berlaku'] = $hasil['total'] - $hasil['tidak_berlaku'];
|
||||||
$hasil['persen'] = $hasil['berlaku'] > 0 ? (int) floor($hasil['terisi'] / $hasil['berlaku'] * 100) : 0;
|
$hasil['persen'] = $hasil['berlaku'] > 0 ? (int) floor($hasil['terisi'] / $hasil['berlaku'] * 100) : 0;
|
||||||
@@ -228,34 +552,79 @@ class KelengkapanPetani
|
|||||||
return $nilai instanceof \BackedEnum ? $nilai->value : $nilai;
|
return $nilai instanceof \BackedEnum ? $nilai->value : $nilai;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hitungKolom(array &$hasil, ?object $model, array $kolom, array $tidakBerlaku = []): void
|
/**
|
||||||
|
* $prioritas: kolom sumber skor/dashboard. Kolom lain juga dicatat di $hasil['lainnya'].
|
||||||
|
*/
|
||||||
|
private function hitungKolom(array &$hasil, ?object $model, array $kolom, array $tidakBerlaku = [], array $prioritas = []): void
|
||||||
{
|
{
|
||||||
foreach ($kolom as $k) {
|
foreach ($kolom as $k) {
|
||||||
$hasil['total']++;
|
$status = match (true) {
|
||||||
|
in_array($k, $tidakBerlaku, true) => 'tidak_berlaku',
|
||||||
if (in_array($k, $tidakBerlaku, true)) {
|
$model && self::terisi($model->{$k}) => 'terisi',
|
||||||
$hasil['tidak_berlaku']++;
|
default => null,
|
||||||
} elseif ($model && self::terisi($model->{$k})) {
|
};
|
||||||
$hasil['terisi']++;
|
$this->catat($hasil, $status, ! in_array($k, $prioritas, true), $k, fn () => self::jawabanKolom($model, $k));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wilayah (provinsi/kabupaten/kecamatan) yang mengikuti desa; termasuk isian prioritas. */
|
||||||
private function hitungNilai(array &$hasil, int $jumlah, bool $terisi): void
|
private function hitungNilai(array &$hasil, int $jumlah, bool $terisi): void
|
||||||
{
|
{
|
||||||
$hasil['total'] += $jumlah;
|
for ($i = 0; $i < $jumlah; $i++) {
|
||||||
$hasil['terisi'] += $terisi ? $jumlah : 0;
|
$this->catat($hasil, $terisi ? 'terisi' : null, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kolom repeater dianggap terisi kalau minimal satu baris mengisinya. */
|
/** Kolom repeater dianggap terisi kalau minimal satu baris mengisinya. */
|
||||||
private function hitungRepeater(array &$hasil, Collection $baris, array $kolom): void
|
private function hitungRepeater(array &$hasil, Collection $baris, array $kolom, array $prioritas = []): void
|
||||||
{
|
{
|
||||||
foreach ($kolom as $k) {
|
foreach ($kolom as $k) {
|
||||||
$hasil['total']++;
|
$terisi = $baris->contains(fn ($row) => self::terisi(data_get($row, $k)));
|
||||||
$hasil['terisi'] += $baris->contains(fn ($row) => self::terisi(data_get($row, $k))) ? 1 : 0;
|
$this->catat($hasil, $terisi ? 'terisi' : null, ! in_array($k, $prioritas, true), $k, fn () => self::jawaban(
|
||||||
|
$baris->map(fn ($row) => is_object($row) ? self::jawabanKolom($row, $k) : self::jawaban(data_get($row, $k), $k))->unique()->all()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $status: 'terisi', 'tidak_berlaku', atau null (kosong).
|
||||||
|
* $jawaban dipanggil hanya saat rincian lainnya dikumpulkan.
|
||||||
|
*/
|
||||||
|
private function catat(array &$hasil, ?string $status, bool $lainnya, ?string $kolom = null, ?Closure $jawaban = null): void
|
||||||
|
{
|
||||||
|
$hasil['total']++;
|
||||||
|
$lainnya && $hasil['lainnya']['total']++;
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
$hasil[$status]++;
|
||||||
|
$lainnya && $hasil['lainnya'][$status]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lainnya && $kolom && $this->rincianLainnya !== null && $status !== 'tidak_berlaku') {
|
||||||
|
$this->rincianLainnya[] = [
|
||||||
|
'grup' => $this->grupRincian,
|
||||||
|
'label' => self::LABEL_KOLOM[$kolom] ?? Str::ucfirst(str_replace('_', ' ', $kolom)),
|
||||||
|
'jawaban' => $jawaban,
|
||||||
|
'terisi' => $status === 'terisi',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Jawaban satu kolom model; kolom *_id ditampilkan nama relasinya (mis. komoditas_id -> komoditas). */
|
||||||
|
private static function jawabanKolom(?object $model, string $kolom): ?string
|
||||||
|
{
|
||||||
|
if (! $model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relasi = str_ends_with($kolom, '_id') ? Str::camel(substr($kolom, 0, -3)) : null;
|
||||||
|
if ($relasi && self::terisi($model->{$kolom}) && method_exists($model, $relasi)) {
|
||||||
|
return $model->{$relasi}?->nama ?? (string) $model->{$kolom};
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::jawaban($model->{$kolom}, $kolom);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Isian yang masih kosong di satu tab.
|
* Isian yang masih kosong di satu tab.
|
||||||
* Setiap item: label, prioritas (wajib/perlu), input (atribut name di form),
|
* Setiap item: label, prioritas (wajib/perlu), input (atribut name di form),
|
||||||
@@ -266,33 +635,30 @@ class KelengkapanPetani
|
|||||||
return $this->semuaKosong()->get($tab, collect());
|
return $this->semuaKosong()->get($tab, collect());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hitungKosong(string $tab): Collection
|
/** Isian kosong semua tab, dikelompokkan per tab (untuk tab Kesiapan Adopsi). */
|
||||||
|
public function semuaKosong(): Collection
|
||||||
{
|
{
|
||||||
$isian = match ($tab) {
|
return $this->semuaKosong ??= $this->semuaItem()->map(fn (array $isian) => collect($isian)
|
||||||
|
->reject(fn (array $item) => $item['terisi'])
|
||||||
|
->map(fn (array $item) => collect($item)->except('terisi')->all())
|
||||||
|
->values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Semua isian WAJIB/PERLU (terisi maupun kosong), per tab. */
|
||||||
|
private function semuaItem(): Collection
|
||||||
|
{
|
||||||
|
return $this->semuaItem ??= collect([
|
||||||
'profil' => $this->profil(),
|
'profil' => $this->profil(),
|
||||||
'lahan' => $this->lahan(),
|
'lahan' => $this->lahan(),
|
||||||
'praktik-budidaya' => $this->praktikBudidaya(),
|
'praktik-budidaya' => $this->praktikBudidaya(),
|
||||||
'sosial-ekonomi' => $this->sosialEkonomi(),
|
'sosial-ekonomi' => $this->sosialEkonomi(),
|
||||||
'akses-pasar' => $this->aksesPasar(),
|
'akses-pasar' => $this->aksesPasar(),
|
||||||
default => [],
|
])->map(fn (array $isian) => array_values($isian));
|
||||||
};
|
|
||||||
|
|
||||||
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
|
private function item(string $label, string $prioritas, string $input, mixed $nilai, array $tambahan = []): array
|
||||||
{
|
{
|
||||||
return $this->itemCek($label, $prioritas, $input, self::terisi($nilai), $tambahan);
|
return $this->itemCek($label, $prioritas, $input, self::terisi($nilai), ['nilai' => $nilai, ...$tambahan]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Untuk isian yang status terisinya dihitung sendiri (mis. repeater). */
|
/** Untuk isian yang status terisinya dihitung sendiri (mis. repeater). */
|
||||||
@@ -303,6 +669,7 @@ class KelengkapanPetani
|
|||||||
'prioritas' => $prioritas,
|
'prioritas' => $prioritas,
|
||||||
'input' => $input,
|
'input' => $input,
|
||||||
'terisi' => $terisi,
|
'terisi' => $terisi,
|
||||||
|
'nilai' => null,
|
||||||
'grup' => null,
|
'grup' => null,
|
||||||
'lahan_id' => null,
|
'lahan_id' => null,
|
||||||
'url' => null,
|
'url' => null,
|
||||||
@@ -324,11 +691,17 @@ class KelengkapanPetani
|
|||||||
$p = $this->petani;
|
$p = $this->petani;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$this->item('Jenis kelamin', self::PERLU, 'jenis_kelamin', $p->jenis_kelamin),
|
$this->item('Jenis kelamin', self::PERLU, 'jenis_kelamin', $p->jenis_kelamin, [
|
||||||
|
'jawaban' => fn () => is_null($p->jenis_kelamin) ? null : ($p->jenis_kelamin ? 'Laki-laki' : 'Perempuan'),
|
||||||
|
]),
|
||||||
$this->item('Tanggal lahir', self::PERLU, 'tanggal_lahir', $p->tanggal_lahir),
|
$this->item('Tanggal lahir', self::PERLU, 'tanggal_lahir', $p->tanggal_lahir),
|
||||||
$this->item('Pendidikan terakhir', self::PERLU, 'pendidikan_terakhir', $p->pendidikan_terakhir),
|
$this->item('Pendidikan terakhir', self::PERLU, 'pendidikan_terakhir', $p->pendidikan_terakhir),
|
||||||
$this->item('Komoditas utama', self::PERLU, 'komoditas_id', $p->komoditas_id),
|
$this->item('Komoditas utama', self::PERLU, 'komoditas_id', $p->komoditas_id, [
|
||||||
$this->item('Desa/kelurahan', self::PERLU, 'desa_kelurahan_id', $p->desa_kelurahan_id),
|
'jawaban' => fn () => self::jawabanKolom($p, 'komoditas_id'),
|
||||||
|
]),
|
||||||
|
$this->item('Desa/kelurahan', self::PERLU, 'desa_kelurahan_id', $p->desa_kelurahan_id, [
|
||||||
|
'jawaban' => fn () => self::jawabanKolom($p, 'desa_kelurahan_id'),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +729,9 @@ class KelengkapanPetani
|
|||||||
return array_filter([
|
return array_filter([
|
||||||
$this->item('Luas lahan', self::PERLU, 'luas_lahan', $l->luas_lahan, $tambahan),
|
$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('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('Desa lahan', self::PERLU, 'desa_kelurahan_id', $l->desa_kelurahan_id, [
|
||||||
|
...$tambahan, 'jawaban' => fn () => self::jawabanKolom($l, 'desa_kelurahan_id'),
|
||||||
|
]),
|
||||||
$this->item('Umur tanaman', self::PERLU, 'umur_tanaman', $l->umur_tanaman, $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('Jumlah tanaman', self::PERLU, 'jumlah_tanaman', $l->jumlah_tanaman, $tambahan),
|
||||||
$this->item('Kondisi tanaman', self::PERLU, 'kondisi_tanaman', $l->kondisi_tanaman, $tambahan),
|
$this->item('Kondisi tanaman', self::PERLU, 'kondisi_tanaman', $l->kondisi_tanaman, $tambahan),
|
||||||
@@ -457,11 +832,14 @@ class KelengkapanPetani
|
|||||||
$this->item('Kesadaran pasar premium', self::WAJIB, 'skor_kesadaran_premium', $ap?->skor_kesadaran_premium),
|
$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->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',
|
$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)),
|
$harga->contains(fn ($h) => self::terisi($h->bentuk_produk) && (float) $h->harga_panen_terakhir > 0),
|
||||||
|
['jawaban' => fn () => self::jawabanHarga($harga, 'harga_panen_terakhir')]),
|
||||||
$this->itemCek('Harga jual tahun lalu', self::PERLU, 'harga_jual',
|
$this->itemCek('Harga jual tahun lalu', self::PERLU, 'harga_jual',
|
||||||
$harga->contains(fn ($h) => (float) $h->harga_tahun_lalu > 0)),
|
$harga->contains(fn ($h) => (float) $h->harga_tahun_lalu > 0),
|
||||||
|
['jawaban' => fn () => self::jawabanHarga($harga, 'harga_tahun_lalu')]),
|
||||||
$this->itemCek('Harga jual dua tahun lalu', self::PERLU, 'harga_jual',
|
$this->itemCek('Harga jual dua tahun lalu', self::PERLU, 'harga_jual',
|
||||||
$harga->contains(fn ($h) => (float) $h->harga_dua_tahun_lalu > 0)),
|
$harga->contains(fn ($h) => (float) $h->harga_dua_tahun_lalu > 0),
|
||||||
|
['jawaban' => fn () => self::jawabanHarga($harga, 'harga_dua_tahun_lalu')]),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Catatan bebas pada data petani (boleh kosong).
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('petani', function (Blueprint $table) {
|
||||||
|
$table->text('catatan')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('petani', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('catatan');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{{--
|
||||||
|
Data petani Terverifikasi: halaman hanya untuk dilihat.
|
||||||
|
Semua tombol simpan/tambah/hapus dan isian form dinonaktifkan (server juga menolak, lihat BatasiAksesPetani).
|
||||||
|
--}}
|
||||||
|
<div class="alert alert-success d-flex align-items-center mb-6" role="alert">
|
||||||
|
<span class="alert-icon rounded me-3"><i class="icon-base ti tabler-lock"></i></span>
|
||||||
|
<span>Data petani ini sudah <strong>terverifikasi</strong> dan hanya bisa dilihat. Data tidak dapat diubah, ditambah, atau dihapus.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@once
|
||||||
|
@push('style')
|
||||||
|
<style>
|
||||||
|
/* Area unggah/hapus foto (dropzone) & elemen yang dikunci tidak bisa diklik. */
|
||||||
|
.data-terkunci .dz-btn-action, .data-terkunci .btn-tambah-foto, .data-terkunci .dropzone,
|
||||||
|
.kunci-terverifikasi { pointer-events: none !important; opacity: .65; }
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const area = document.querySelector('.content-wrapper');
|
||||||
|
if (!area) return;
|
||||||
|
area.classList.add('data-terkunci');
|
||||||
|
|
||||||
|
// Tombol hapus/tambah yang bukan bagian form (dijalankan lewat JS/AJAX) & tautan ke halaman tambah.
|
||||||
|
const TOMBOL_UBAH = [
|
||||||
|
'[class*="delete"]', '[class*="hapus"]', '.btn-danger', '.btn-outline-danger',
|
||||||
|
'.btn-tambah-foto', '.dz-btn-action', '.add-new', 'a[href$="/create"]',
|
||||||
|
].join(',');
|
||||||
|
|
||||||
|
// Bagian yang tetap aktif: tab, ringkasan kelengkapan, pencarian/filter (form GET).
|
||||||
|
const dikecualikan = (el) => el.closest('.nav, #kelengkapan-petani, form[method="GET" i], form:not([method])');
|
||||||
|
|
||||||
|
function kunci() {
|
||||||
|
// Semua isian & tombol di form yang menyimpan data (POST/PUT/DELETE).
|
||||||
|
area.querySelectorAll('form[method="POST" i]').forEach((form) => {
|
||||||
|
form.querySelectorAll('input:not([type="hidden"]), select, textarea, button').forEach((el) => {
|
||||||
|
el.disabled = true;
|
||||||
|
});
|
||||||
|
form.addEventListener('submit', (e) => e.preventDefault());
|
||||||
|
});
|
||||||
|
|
||||||
|
area.querySelectorAll(TOMBOL_UBAH).forEach((el) => {
|
||||||
|
if (dikecualikan(el)) return;
|
||||||
|
el.classList.add('kunci-terverifikasi');
|
||||||
|
el.setAttribute('aria-disabled', 'true');
|
||||||
|
el.setAttribute('tabindex', '-1');
|
||||||
|
if ('disabled' in el) el.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setelah plugin (select2, flatpickr, dropzone, repeater) selesai dipasang.
|
||||||
|
window.addEventListener('load', () => { kunci(); setTimeout(kunci, 500); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endonce
|
||||||
@@ -112,13 +112,19 @@
|
|||||||
...gayaTombolSwal(),
|
...gayaTombolSwal(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tandai Lengkap: wajib mencentang pernyataan sebelum bisa lanjut.
|
// Tandai Lengkap: tampilkan pernyataan; menekan tombol konfirmasi berarti menyetujuinya.
|
||||||
if (pernyataan) {
|
if (pernyataan) {
|
||||||
opsi.input = 'checkbox';
|
const isi = document.createElement('div');
|
||||||
opsi.inputValue = 0;
|
const kotak = document.createElement('div');
|
||||||
opsi.inputPlaceholder = pernyataan;
|
kotak.className = 'border rounded p-3 mb-3 text-start small';
|
||||||
opsi.inputValidator = (dicentang) => !dicentang && 'Centang pernyataan terlebih dahulu.';
|
kotak.textContent = pernyataan;
|
||||||
opsi.customClass.input = 'text-start small';
|
const catatan = document.createElement('p');
|
||||||
|
catatan.className = 'mb-0';
|
||||||
|
catatan.textContent = form.dataset.konfirmasi;
|
||||||
|
isi.append(kotak, catatan);
|
||||||
|
|
||||||
|
delete opsi.text;
|
||||||
|
opsi.html = isi;
|
||||||
}
|
}
|
||||||
|
|
||||||
Swal.fire(opsi).then((r) => { if (r.isConfirmed) kirim(); });
|
Swal.fire(opsi).then((r) => { if (r.isConfirmed) kirim(); });
|
||||||
|
|||||||
@@ -125,6 +125,24 @@
|
|||||||
@endif
|
@endif
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{{-- Menu Verifikasi: hanya super_admin, admin, koordinator (enumerator tidak). --}}
|
||||||
|
@if($isAdmin || $isKoordinator)
|
||||||
|
<li class="menu-item {{ request()->routeIs('monitoring-verifikasi.*') ? '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-rosette-discount-check"></i></span>
|
||||||
|
<div data-i18n="Verifikasi">Verifikasi</div>
|
||||||
|
</a>
|
||||||
|
<ul class="menu-sub">
|
||||||
|
<li class="menu-item {{ request()->routeIs('monitoring-verifikasi.petani.*') ? 'active' : '' }}">
|
||||||
|
<a href="{{ route('monitoring-verifikasi.petani.monitoring') }}" 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>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($isAdmin && $appName === 'AgroPro')
|
@if($isAdmin && $appName === 'AgroPro')
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
@use('App\Services\Produktivitas\KelengkapanPetani')
|
||||||
|
|
||||||
|
@if (($bagian ?? 'tabel') === 'rincian')
|
||||||
|
{{-- ============ Isi kartu rincian: petani yang diinput enumerator ini ============ --}}
|
||||||
|
<div class="d-flex align-items-center mb-6">
|
||||||
|
<div class="avatar avatar-md me-3 flex-shrink-0">
|
||||||
|
<span class="avatar-initial rounded bg-label-primary">{{ Str::upper(Str::substr($user->name, 0, 2)) }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="min-width: 0;">
|
||||||
|
<h6 class="mb-0 text-truncate">{{ $user->name }}</h6>
|
||||||
|
<small class="d-block text-body-secondary">{{ $daftarPetani->count() }} petani diinput</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive text-nowrap">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="border-top">
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Nama Petani</th>
|
||||||
|
<th class="text-center" title="Pertanyaan skor Kesiapan Adopsi">Skor</th>
|
||||||
|
<th class="text-center" title="Isian yang dipakai dashboard">Wajib</th>
|
||||||
|
<th class="text-center" title="Isian lainnya">Lainnya</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($daftarPetani as $item)
|
||||||
|
@php $r = $ringkasan[$item->id]; @endphp
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td class="text-wrap">
|
||||||
|
<a href="{{ route('petani.edit', $item->id) }}" class="text-heading fw-medium">{{ $item->nama }}</a>
|
||||||
|
<small class="d-block text-body-secondary">{{ $item->komoditas?->nama ?? '-' }}</small>
|
||||||
|
</td>
|
||||||
|
@foreach (['skor', 'wajib', 'lainnya'] as $jenis)
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-label-{{ KelengkapanPetani::warna($r[$jenis]) }}">{{ $r[$jenis]['terisi'] }}/{{ $r[$jenis]['total'] }}</span>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-4">Belum ada petani.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- ============ Tabel enumerator; klik jumlah petani → kartu rincian ============ --}}
|
||||||
|
<div class="monitoring-split">
|
||||||
|
<div class="kolom-tabel">
|
||||||
|
<form method="GET" action="{{ route('monitoring-verifikasi.petani.monitoring') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0 mb-4">
|
||||||
|
<input type="hidden" name="sub" value="enumerator">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label class="me-2 text-nowrap">Show</label>
|
||||||
|
<select name="per_page_enumerator" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||||
|
@foreach ($perPage as $n)
|
||||||
|
<option value="{{ $n }}" @selected($enumerator->perPage() == $n)>{{ $n }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="ms-2 text-nowrap">entries</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="input-group input-group-sm w-auto">
|
||||||
|
<input type="text" name="search_enumerator" class="form-control" placeholder="Cari nama enumerator..." value="{{ request('search_enumerator') }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary"><i class="icon-base ti tabler-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive text-nowrap">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="border-top">
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Enumerator</th>
|
||||||
|
<th class="text-center">Jumlah Petani</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($enumerator as $index => $user)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $enumerator->firstItem() + $index }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-heading fw-medium d-block">{{ $user->name }}</span>
|
||||||
|
<small class="text-body-secondary">{{ $user->email }}</small>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
@if ($user->jumlah_petani > 0)
|
||||||
|
<button type="button" class="btn btn-sm btn-label-primary btn-buka-rincian"
|
||||||
|
data-url="{{ route('monitoring-verifikasi.petani.monitoring.enumerator', $user->id) }}"
|
||||||
|
data-judul="Petani oleh {{ $user->name }}" title="Lihat petani yang diinput">
|
||||||
|
{{ $user->jumlah_petani }} petani
|
||||||
|
</button>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-label-secondary">0 petani</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-4">Tidak ada data ditemukan.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center justify-content-md-end border-top pt-4">
|
||||||
|
{{ $enumerator->links('pagination::bootstrap-5') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kolom-detail" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
@extends('layout.main')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$menu = $menu ?? 'petani';
|
||||||
|
|
||||||
|
$tabs = [
|
||||||
|
'monitoring' => ['label' => 'Monitoring', 'ikon' => 'tabler-chart-dots'],
|
||||||
|
'verifikasi' => ['label' => 'Verifikasi', 'ikon' => 'tabler-rosette-discount-check'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
|
{{-- Vuexy: justified pills (nav-fill) → tiap tab sama lebar. --}}
|
||||||
|
<div class="nav-align-top">
|
||||||
|
<ul class="nav nav-pills nav-fill mb-6" role="tablist">
|
||||||
|
@foreach ($tabs as $kunci => $tab)
|
||||||
|
@php
|
||||||
|
$namaRoute = "monitoring-verifikasi.{$menu}.{$kunci}";
|
||||||
|
$tersedia = Route::has($namaRoute);
|
||||||
|
$aktif = request()->routeIs($namaRoute . '*');
|
||||||
|
@endphp
|
||||||
|
<li class="nav-item">
|
||||||
|
<a @class(['nav-link', 'active' => $aktif, 'disabled' => ! $tersedia])
|
||||||
|
href="{{ $tersedia ? route($namaRoute) : 'javascript:void(0);' }}"
|
||||||
|
@if ($aktif) aria-current="page" @endif
|
||||||
|
@unless ($tersedia) aria-disabled="true" @endunless>
|
||||||
|
<span class="d-none d-sm-inline-flex align-items-center">
|
||||||
|
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm me-1_5"></i>{{ $tab['label'] }}
|
||||||
|
</span>
|
||||||
|
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm d-sm-none"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Sub-tab: halaman turunan cukup mendefinisikan $idSubTab & $subTabs (label, ikon, view, jumlah opsional). --}}
|
||||||
|
@isset($subTabs)
|
||||||
|
@php $subTabAktif = array_key_exists(request('sub'), $subTabs) ? request('sub') : array_key_first($subTabs); @endphp
|
||||||
|
<div class="nav-align-top nav-tabs-shadow">
|
||||||
|
<ul class="nav nav-tabs nav-fill" role="tablist">
|
||||||
|
@foreach ($subTabs as $kunci => $tab)
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button type="button" role="tab"
|
||||||
|
@class(['nav-link', 'active' => $kunci === $subTabAktif])
|
||||||
|
data-bs-toggle="tab" data-bs-target="#{{ $idSubTab }}-{{ $kunci }}"
|
||||||
|
aria-controls="{{ $idSubTab }}-{{ $kunci }}" aria-selected="{{ $kunci === $subTabAktif ? 'true' : 'false' }}">
|
||||||
|
<span class="d-none d-sm-inline-flex align-items-center">
|
||||||
|
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm me-1_5"></i>{{ $tab['label'] }}
|
||||||
|
@isset($tab['jumlah'])
|
||||||
|
<span class="badge rounded-pill badge-center h-px-20 w-px-20 bg-label-primary ms-1_5">{{ $tab['jumlah'] }}</span>
|
||||||
|
@endisset
|
||||||
|
</span>
|
||||||
|
<i class="icon-base ti {{ $tab['ikon'] }} icon-sm d-sm-none"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
@foreach ($subTabs as $kunci => $tab)
|
||||||
|
<div @class(['tab-pane fade', 'show active' => $kunci === $subTabAktif]) id="{{ $idSubTab }}-{{ $kunci }}" role="tabpanel">
|
||||||
|
@include($tab['view'])
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="avatar avatar-md mx-auto mb-2">
|
||||||
|
<span class="avatar-initial rounded bg-label-secondary"><i class="icon-base ti tabler-layout-dashboard icon-26px"></i></span>
|
||||||
|
</span>
|
||||||
|
<p class="mb-0 text-body-secondary">Konten tab belum dibuat.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endisset
|
||||||
|
|
||||||
|
{{-- Kartu rincian: disalin otomatis ke setiap .kolom-detail yang masih kosong. --}}
|
||||||
|
<template id="template-kartu-rincian">
|
||||||
|
<div class="card shadow-none border h-100 kartu-rincian">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center border-bottom">
|
||||||
|
<h5 class="card-title mb-0 kartu-rincian-judul">Rincian</h5>
|
||||||
|
<button type="button" class="btn-close btn-tutup-rincian" aria-label="Tutup"></button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-6 kartu-rincian-isi"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('style')
|
||||||
|
<style>
|
||||||
|
/* Tabel penuh; saat rincian dibuka, tabel menyusut & kartu rincian bergeser masuk dari kanan. */
|
||||||
|
.monitoring-split { display: flex; gap: 0; transition: gap .3s ease; }
|
||||||
|
.monitoring-split .kolom-tabel { flex: 1 1 100%; min-width: 0; transition: flex-basis .3s ease; }
|
||||||
|
.monitoring-split .kolom-detail {
|
||||||
|
flex: 0 0 0; max-width: 0; min-width: 0; opacity: 0; overflow: hidden;
|
||||||
|
transform: translateX(1.5rem);
|
||||||
|
transition: flex-basis .3s ease, max-width .3s ease, opacity .3s ease, transform .3s ease;
|
||||||
|
}
|
||||||
|
.monitoring-split.terbuka { gap: 1.5rem; }
|
||||||
|
.monitoring-split.terbuka .kolom-tabel { flex-basis: 58%; }
|
||||||
|
.monitoring-split.terbuka .kolom-detail { flex-basis: 42%; max-width: 42%; opacity: 1; transform: none; }
|
||||||
|
|
||||||
|
/* Kartu rincian ikut terlihat saat halaman digulir; isinya bisa digulir sendiri. */
|
||||||
|
.kartu-rincian { position: sticky; top: 1rem; max-height: calc(100vh - 2rem); }
|
||||||
|
.kartu-rincian .card-body { overflow-y: auto; }
|
||||||
|
|
||||||
|
/* Baris tabel yang bisa diklik untuk membuka rincian. */
|
||||||
|
tr.btn-buka-rincian { cursor: pointer; }
|
||||||
|
|
||||||
|
/* Layar kecil: rincian tampil di bawah tabel. */
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.monitoring-split { flex-direction: column; }
|
||||||
|
.monitoring-split.terbuka .kolom-tabel,
|
||||||
|
.monitoring-split.terbuka .kolom-detail { flex-basis: auto; max-width: 100%; }
|
||||||
|
.kartu-rincian { position: static; max-height: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const memuat = '<div class="text-center py-6"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||||
|
const layarKecil = () => window.matchMedia('(max-width: 991.98px)').matches;
|
||||||
|
const templateKartu = document.getElementById('template-kartu-rincian');
|
||||||
|
|
||||||
|
document.querySelectorAll('.monitoring-split').forEach(function (split) {
|
||||||
|
const kolomTabel = split.querySelector('.kolom-tabel');
|
||||||
|
const kolomDetail = split.querySelector('.kolom-detail');
|
||||||
|
if (!kolomDetail.children.length) kolomDetail.append(templateKartu.content.cloneNode(true));
|
||||||
|
const judul = kolomDetail.querySelector('.kartu-rincian-judul');
|
||||||
|
const isi = kolomDetail.querySelector('.kartu-rincian-isi');
|
||||||
|
let permintaan = 0; // abaikan respons lama kalau tombol lain sudah diklik
|
||||||
|
let tombolAktif = null;
|
||||||
|
|
||||||
|
const tandai = (tombol) => {
|
||||||
|
kolomTabel.querySelectorAll('tr.table-active').forEach((tr) => tr.classList.remove('table-active'));
|
||||||
|
tombol?.closest('tr')?.classList.add('table-active');
|
||||||
|
tombolAktif = tombol;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tutup = () => {
|
||||||
|
split.classList.remove('terbuka');
|
||||||
|
kolomDetail.setAttribute('aria-hidden', 'true');
|
||||||
|
tandai(null);
|
||||||
|
permintaan++;
|
||||||
|
};
|
||||||
|
split.tutupRincian = tutup;
|
||||||
|
|
||||||
|
kolomDetail.querySelector('.btn-tutup-rincian').addEventListener('click', tutup);
|
||||||
|
|
||||||
|
kolomTabel.addEventListener('click', async function (e) {
|
||||||
|
const tombol = e.target.closest('.btn-buka-rincian');
|
||||||
|
if (!tombol) return;
|
||||||
|
// Tautan di dalam baris yang bisa diklik tetap berfungsi sebagai tautan.
|
||||||
|
const tautan = e.target.closest('a');
|
||||||
|
if (tautan && tautan !== tombol && tombol.contains(tautan)) return;
|
||||||
|
|
||||||
|
// Klik tombol yang sama lagi → tutup.
|
||||||
|
if (tombol === tombolAktif && split.classList.contains('terbuka')) return tutup();
|
||||||
|
|
||||||
|
const nomor = ++permintaan;
|
||||||
|
tandai(tombol);
|
||||||
|
judul.textContent = tombol.dataset.judul || 'Rincian';
|
||||||
|
isi.innerHTML = memuat;
|
||||||
|
isi.scrollTop = 0;
|
||||||
|
split.classList.add('terbuka');
|
||||||
|
kolomDetail.setAttribute('aria-hidden', 'false');
|
||||||
|
if (layarKecil()) kolomDetail.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(tombol.dataset.url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
|
if (!res.ok) throw new Error(res.status);
|
||||||
|
const html = await res.text();
|
||||||
|
if (nomor === permintaan) isi.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
if (nomor === permintaan) {
|
||||||
|
isi.innerHTML = '<div class="alert alert-danger mb-0">Gagal memuat rincian. Coba lagi.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Esc menutup rincian yang sedang terbuka.
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
document.querySelectorAll('.monitoring-split.terbuka').forEach((split) => split.tutupRincian?.());
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@extends('monitoring-verifikasi.index')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$idSubTab = 'monitoring';
|
||||||
|
$subTabs = [
|
||||||
|
'petani' => ['label' => 'Petani', 'ikon' => 'tabler-user-circle', 'view' => 'monitoring-verifikasi.petani-monitoring'],
|
||||||
|
'enumerator' => ['label' => 'Enumerator', 'ikon' => 'tabler-clipboard-list', 'view' => 'monitoring-verifikasi.enumerator'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
@use('App\Services\Produktivitas\KelengkapanPetani')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$langkah = [
|
||||||
|
1 => ['jenis' => 'skor', 'label' => 'Input Skor'],
|
||||||
|
2 => ['jenis' => 'wajib', 'label' => 'Input Wajib'],
|
||||||
|
3 => ['jenis' => 'lainnya', 'label' => 'Input Lainnya'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if (($bagian ?? 'tabel') === 'rincian')
|
||||||
|
{{-- ============ Isi kartu verifikasi: profil + 3 langkah pemeriksaan ============ --}}
|
||||||
|
@php $skorIndeks = $petani->skor?->skor_kesiapan_adopsi; @endphp
|
||||||
|
|
||||||
|
<div class="stepper-verifikasi">
|
||||||
|
@include('monitoring-verifikasi.petani-monitoring', ['bagian' => 'profil'])
|
||||||
|
|
||||||
|
<div class="row g-4 mb-6">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Enumerator</label>
|
||||||
|
<input type="text" class="form-control" readonly value="{{ $petani->creator?->name ?? '-' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Skor Indeks</label>
|
||||||
|
<input type="text" class="form-control" readonly
|
||||||
|
value="{{ is_null($skorIndeks) ? '-' : number_format($skorIndeks, 1, ',', '.') . '/5 · ' . $petani->segmen() }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($petani->catatan)
|
||||||
|
{{-- Catatan saat terakhir dikembalikan ke Draf --}}
|
||||||
|
<div class="alert alert-warning mb-6" role="alert">
|
||||||
|
<h6 class="alert-heading mb-1">Catatan sebelumnya</h6>
|
||||||
|
<p class="mb-0" style="white-space: pre-line;">{{ $petani->catatan }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Penanda langkah --}}
|
||||||
|
<div class="d-flex justify-content-between gap-2 border-bottom pb-4 mb-6">
|
||||||
|
@foreach ($langkah as $n => $l)
|
||||||
|
@php $hitung = $ringkasan[$l['jenis']]; @endphp
|
||||||
|
<div class="d-flex align-items-center gap-2 langkah-item" data-langkah="{{ $n }}">
|
||||||
|
<span class="avatar avatar-sm flex-shrink-0">
|
||||||
|
<span @class(['avatar-initial rounded-circle langkah-nomor', 'bg-primary text-white' => $n === 1, 'bg-label-secondary' => $n !== 1])>{{ $n }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="lh-sm">
|
||||||
|
<span class="d-block fw-medium small text-heading">{{ $l['label'] }}</span>
|
||||||
|
<small class="text-body-secondary">{{ $hitung['terisi'] }}/{{ $hitung['total'] }}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Isi tiap langkah: periksa semua isian, lalu klik tombol di bawah --}}
|
||||||
|
@foreach ($langkah as $n => $l)
|
||||||
|
<form class="pt-0 langkah-pane {{ $n === 1 ? '' : 'd-none' }}" data-langkah="{{ $n }}" onsubmit="return false;">
|
||||||
|
@include('monitoring-verifikasi.petani-monitoring', ['bagian' => 'isian', 'rincian' => $rincian[$l['jenis']]])
|
||||||
|
|
||||||
|
{{-- Langkah terakhir punya tombolnya sendiri di bawah --}}
|
||||||
|
@if ($n < count($langkah))
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
@if ($n > 1)
|
||||||
|
<button type="button" class="btn btn-label-secondary" data-ke-langkah="{{ $n - 1 }}">
|
||||||
|
<i class="icon-base ti tabler-arrow-left icon-sm me-1"></i>Kembali
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
<button type="button" class="btn btn-primary flex-grow-1" data-ke-langkah="{{ $n + 1 }}">
|
||||||
|
Lengkap<i class="icon-base ti tabler-arrow-right icon-sm ms-1"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</form>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
{{-- Langkah terakhir: Belum Lengkap (kembali ke Draf + catatan) atau Lengkap & Verifikasi --}}
|
||||||
|
{{-- Baris 1: Kembali | Belum Lengkap (sama lebar). Baris 2: Lengkap & Verifikasi (penuh). --}}
|
||||||
|
<div class="langkah-pane d-none" data-langkah="{{ count($langkah) }}">
|
||||||
|
<div class="row g-2 mb-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<button type="button" class="btn btn-label-secondary w-100" data-ke-langkah="{{ count($langkah) - 1 }}">
|
||||||
|
<i class="icon-base ti tabler-arrow-left icon-sm me-1"></i>Kembali
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<form method="POST" action="{{ route('monitoring-verifikasi.petani.verifikasi.kembalikan-draf', $petani->id) }}"
|
||||||
|
class="form-kembalikan-draf" data-nama="{{ $petani->nama }}"
|
||||||
|
data-judul="Data Belum Lengkap" data-pesan="Tuliskan apa yang perlu dilengkapi atau diperbaiki oleh enumerator.">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="catatan" value="">
|
||||||
|
<button type="submit" class="btn btn-label-danger w-100">
|
||||||
|
<i class="icon-base ti tabler-file-alert icon-sm me-1"></i>Belum Lengkap
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('monitoring-verifikasi.petani.verifikasi.update', $petani->id) }}"
|
||||||
|
class="form-verifikasi-petani" data-nama="{{ $petani->nama }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="icon-base ti tabler-rosette-discount-check icon-sm me-1"></i>Lengkap & Verifikasi
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
{{-- ============ Tabel petani berstatus Lengkap; klik di mana saja pada baris → kartu verifikasi ============ --}}
|
||||||
|
<div class="monitoring-split">
|
||||||
|
<div class="kolom-tabel">
|
||||||
|
<form method="GET" action="{{ route('monitoring-verifikasi.petani.verifikasi') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0 mb-4">
|
||||||
|
<input type="hidden" name="sub" value="lengkap">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label class="me-2 text-nowrap">Show</label>
|
||||||
|
<select name="per_page" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||||
|
@foreach ($perPage as $n)
|
||||||
|
<option value="{{ $n }}" @selected($petaniLengkap->perPage() == $n)>{{ $n }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="ms-2 text-nowrap">entries</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="input-group input-group-sm w-auto">
|
||||||
|
<input type="text" name="search" class="form-control" placeholder="Cari nama petani..." value="{{ request('search') }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary"><i class="icon-base ti tabler-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive text-nowrap">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="border-top">
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Nama Petani</th>
|
||||||
|
<th>Komoditas</th>
|
||||||
|
<th>Enumerator</th>
|
||||||
|
@foreach ($langkah as $l)
|
||||||
|
<th class="text-center">{{ $l['label'] }}</th>
|
||||||
|
@endforeach
|
||||||
|
<th>Terakhir Diperbarui</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($petaniLengkap as $index => $item)
|
||||||
|
@php $r = $ringkasan[$item->id]; @endphp
|
||||||
|
<tr class="btn-buka-rincian"
|
||||||
|
data-url="{{ route('monitoring-verifikasi.petani.verifikasi.show', $item->id) }}"
|
||||||
|
data-judul="Verifikasi {{ $item->nama }}" title="Klik untuk memeriksa & memverifikasi">
|
||||||
|
<td>{{ $petaniLengkap->firstItem() + $index }}</td>
|
||||||
|
<td><span class="text-heading fw-medium">{{ $item->nama }}</span></td>
|
||||||
|
<td>{{ $item->komoditas?->nama ?? '-' }}</td>
|
||||||
|
<td><span class="text-body-secondary">{{ $item->creator?->name ?? '-' }}</span></td>
|
||||||
|
@foreach ($langkah as $l)
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-label-{{ KelengkapanPetani::warna($r[$l['jenis']]) }}">{{ $r[$l['jenis']]['terisi'] }}/{{ $r[$l['jenis']]['total'] }}</span>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
<td>
|
||||||
|
<span class="d-block">{{ $item->updated_at?->translatedFormat('d M Y') ?? '-' }}</span>
|
||||||
|
<small class="text-body-secondary">{{ $item->updated_at?->format('H:i') }}</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4">Belum ada data petani yang ditandai lengkap.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center justify-content-md-end border-top pt-4">
|
||||||
|
{{ $petaniLengkap->links('pagination::bootstrap-5') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kolom-detail" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
@use('App\Services\Produktivitas\KelengkapanPetani')
|
||||||
|
|
||||||
|
@if (($bagian ?? 'tabel') === 'profil')
|
||||||
|
{{-- ============ Profil singkat petani (dipakai juga di kartu verifikasi) ============ --}}
|
||||||
|
@php $desa = $petani->desaKelurahan; @endphp
|
||||||
|
<div class="d-flex align-items-center mb-6">
|
||||||
|
<div class="avatar avatar-xl me-3 flex-shrink-0">
|
||||||
|
<img src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/3.jpg') }}"
|
||||||
|
alt="Foto {{ $petani->nama }}" class="rounded" style="object-fit: cover;">
|
||||||
|
</div>
|
||||||
|
<div style="min-width: 0;">
|
||||||
|
<h6 class="mb-0 text-truncate">{{ $petani->nama }}</h6>
|
||||||
|
<small class="d-block text-body-secondary">{{ is_null($petani->umur) ? 'Umur belum diisi' : $petani->umur . ' tahun' }}</small>
|
||||||
|
<small class="d-block text-body-secondary">
|
||||||
|
{{ $desa ? collect([$desa->nama, $desa->kecamatan?->nama, $desa->kecamatan?->kabupatenKota?->nama])->filter()->implode(', ') : 'Desa belum diisi' }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif (($bagian ?? 'tabel') === 'isian')
|
||||||
|
{{-- ============ Daftar pertanyaan -> jawaban (dipakai juga di kartu verifikasi) ============ --}}
|
||||||
|
@forelse ($rincian as $grup => $baris)
|
||||||
|
<h6 class="pb-2 border-bottom mb-4">{{ $grup }}</h6>
|
||||||
|
@foreach ($baris as $r)
|
||||||
|
@php $kosong = ! $r['terisi'] || $r['jawaban'] === null; @endphp
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="form-label">{{ $r['label'] }}</label>
|
||||||
|
@if (! $kosong && mb_strlen($r['jawaban']) > 60)
|
||||||
|
<textarea class="form-control" rows="2" readonly>{{ $r['jawaban'] }}</textarea>
|
||||||
|
@else
|
||||||
|
<input type="text" @class(['form-control', 'is-invalid' => $kosong]) readonly
|
||||||
|
value="{{ $kosong ? 'Belum diisi' : $r['jawaban'] }}">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@empty
|
||||||
|
<p class="text-center text-body-secondary mb-6">Tidak ada isian.</p>
|
||||||
|
@endforelse
|
||||||
|
@elseif (($bagian ?? 'tabel') === 'rincian')
|
||||||
|
{{-- ============ Isi kartu rincian: profil singkat + pertanyaan -> jawaban ============ --}}
|
||||||
|
@php $skorIndeks = $petani->skor?->skor_kesiapan_adopsi; @endphp
|
||||||
|
|
||||||
|
<form class="pt-0" onsubmit="return false;">
|
||||||
|
@include('monitoring-verifikasi.petani-monitoring', ['bagian' => 'profil'])
|
||||||
|
|
||||||
|
<div class="row g-4 mb-6">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Skor Indeks</label>
|
||||||
|
<input type="text" class="form-control" readonly
|
||||||
|
value="{{ is_null($skorIndeks) ? '-' : number_format($skorIndeks, 1, ',', '.') . '/5 · ' . $petani->segmen() }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">Terisi</label>
|
||||||
|
<input type="text" class="form-control" readonly
|
||||||
|
value="{{ $ringkasan['terisi'] }}/{{ $ringkasan['total'] }} ({{ $ringkasan['total'] ? floor($ringkasan['terisi'] / $ringkasan['total'] * 100) : 0 }}%)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('monitoring-verifikasi.petani-monitoring', ['bagian' => 'isian'])
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
{{-- ============ Tabel petani; klik badge Input → kartu rincian ============ --}}
|
||||||
|
@php
|
||||||
|
$judulInput = ['skor' => 'Input Skor', 'wajib' => 'Input Wajib', 'lainnya' => 'Input Lainnya'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="monitoring-split">
|
||||||
|
<div class="kolom-tabel">
|
||||||
|
<form method="GET" action="{{ route('monitoring-verifikasi.petani.monitoring') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0 mb-4">
|
||||||
|
<input type="hidden" name="sub" value="petani">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label class="me-2 text-nowrap">Show</label>
|
||||||
|
<select name="per_page" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||||
|
@foreach ($perPage as $n)
|
||||||
|
<option value="{{ $n }}" @selected($petani->perPage() == $n)>{{ $n }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="ms-2 text-nowrap">entries</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="input-group input-group-sm w-auto">
|
||||||
|
<input type="text" name="search" class="form-control" placeholder="Cari nama petani..." value="{{ request('search') }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary"><i class="icon-base ti tabler-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive text-nowrap">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="border-top">
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Nama Petani</th>
|
||||||
|
<th>Komoditas</th>
|
||||||
|
<th>Enumerator</th>
|
||||||
|
<th class="text-center" title="Pertanyaan yang dinilai di skor Kesiapan Adopsi">Input Skor</th>
|
||||||
|
<th class="text-center" title="Isian yang dipakai dashboard">Input Wajib</th>
|
||||||
|
<th class="text-center" title="Isian lainnya di Profil Petani">Input Lainnya</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($petani as $index => $item)
|
||||||
|
@php $r = $ringkasan[$item->id]; @endphp
|
||||||
|
<tr>
|
||||||
|
<td>{{ $petani->firstItem() + $index }}</td>
|
||||||
|
<td><a href="{{ route('petani.edit', $item->id) }}" class="text-heading fw-medium">{{ $item->nama }}</a></td>
|
||||||
|
<td>{{ $item->komoditas?->nama ?? '-' }}</td>
|
||||||
|
<td><span class="text-body-secondary">{{ $item->creator?->name ?? '-' }}</span></td>
|
||||||
|
@foreach ($judulInput as $jenis => $judul)
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-label-{{ KelengkapanPetani::warna($r[$jenis]) }} btn-buka-rincian"
|
||||||
|
data-url="{{ route('monitoring-verifikasi.petani.monitoring.detail', [$item->id, $jenis]) }}"
|
||||||
|
data-judul="Rincian {{ $judul }}" title="Lihat rincian {{ strtolower($judul) }}">
|
||||||
|
{{ $r[$jenis]['terisi'] }}/{{ $r[$jenis]['total'] }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center py-4">Tidak ada data ditemukan.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center justify-content-md-end border-top pt-4">
|
||||||
|
{{ $petani->links('pagination::bootstrap-5') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kolom-detail" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
@if (($bagian ?? 'tabel') === 'rincian')
|
||||||
|
{{-- ============ Isi kartu: profil + siapa & kapan diverifikasi + Kembalikan ke Draf ============ --}}
|
||||||
|
<form class="pt-0" onsubmit="return false;">
|
||||||
|
@include('monitoring-verifikasi.petani-monitoring', ['bagian' => 'profil'])
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="form-label">Komoditas</label>
|
||||||
|
<input type="text" class="form-control" readonly value="{{ $petani->komoditas?->nama ?? '-' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="form-label">Enumerator</label>
|
||||||
|
<input type="text" class="form-control" readonly value="{{ $petani->creator?->name ?? '-' }}">
|
||||||
|
</div>
|
||||||
|
{{-- Data terverifikasi dikunci, jadi updated_by / updated_at = yang & waktu memverifikasi --}}
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="form-label">Diverifikasi Oleh</label>
|
||||||
|
<input type="text" class="form-control" readonly
|
||||||
|
value="{{ $petani->updater?->name ?? '-' }}{{ $petani->updater?->role ? ' · ' . $petani->updater->role->label() : '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="form-label">Waktu Verifikasi</label>
|
||||||
|
<input type="text" class="form-control" readonly value="{{ $petani->updated_at?->translatedFormat('d M Y, H:i') ?? '-' }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{-- Ada temuan setelah diverifikasi: kembalikan ke Draf dengan catatan --}}
|
||||||
|
<form method="POST" action="{{ route('monitoring-verifikasi.petani.verifikasi.kembalikan-draf', $petani->id) }}"
|
||||||
|
class="form-kembalikan-draf" data-nama="{{ $petani->nama }}"
|
||||||
|
data-judul="Kembalikan ke Draf" data-pesan="Data ini sudah terverifikasi. Tuliskan temuan yang perlu diperbaiki oleh enumerator.">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="catatan" value="">
|
||||||
|
<button type="submit" class="btn btn-label-danger w-100">
|
||||||
|
<i class="icon-base ti tabler-arrow-back-up icon-sm me-1"></i>Kembalikan ke Draf
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
{{-- ============ Tabel petani terverifikasi; klik di mana saja pada baris → kartu rincian ============ --}}
|
||||||
|
<div class="monitoring-split">
|
||||||
|
<div class="kolom-tabel">
|
||||||
|
<form method="GET" action="{{ route('monitoring-verifikasi.petani.verifikasi') }}" class="d-flex justify-content-between align-items-center row gap-3 gap-md-0 mb-4">
|
||||||
|
<input type="hidden" name="sub" value="terverifikasi">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<label class="me-2 text-nowrap">Show</label>
|
||||||
|
<select name="per_page_terverifikasi" class="form-select form-select-sm w-auto" onchange="this.form.submit()">
|
||||||
|
@foreach ($perPage as $n)
|
||||||
|
<option value="{{ $n }}" @selected($petaniTerverifikasi->perPage() == $n)>{{ $n }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label class="ms-2 text-nowrap">entries</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<div class="input-group input-group-sm w-auto">
|
||||||
|
<input type="text" name="search_terverifikasi" class="form-control" placeholder="Cari nama petani..." value="{{ request('search_terverifikasi') }}">
|
||||||
|
<button type="submit" class="btn btn-outline-primary"><i class="icon-base ti tabler-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive text-nowrap">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="border-top">
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Nama Petani</th>
|
||||||
|
<th>Komoditas</th>
|
||||||
|
<th>Desa</th>
|
||||||
|
<th>Enumerator</th>
|
||||||
|
<th>Diverifikasi Oleh</th>
|
||||||
|
<th>Terakhir Diperbarui</th>
|
||||||
|
<th class="text-center">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($petaniTerverifikasi as $index => $item)
|
||||||
|
<tr class="btn-buka-rincian"
|
||||||
|
data-url="{{ route('monitoring-verifikasi.petani.verifikasi.show', $item->id) }}"
|
||||||
|
data-judul="{{ $item->nama }}" title="Klik untuk melihat rincian verifikasi">
|
||||||
|
<td>{{ $petaniTerverifikasi->firstItem() + $index }}</td>
|
||||||
|
<td><span class="text-heading fw-medium">{{ $item->nama }}</span></td>
|
||||||
|
<td>{{ $item->komoditas?->nama ?? '-' }}</td>
|
||||||
|
<td>{{ $item->desaKelurahan?->nama ?? '-' }}</td>
|
||||||
|
<td><span class="text-body-secondary">{{ $item->creator?->name ?? '-' }}</span></td>
|
||||||
|
{{-- Data terverifikasi dikunci, jadi updated_by = yang memverifikasi --}}
|
||||||
|
<td>
|
||||||
|
<span class="d-block text-heading">{{ $item->updater?->name ?? '-' }}</span>
|
||||||
|
@if ($item->updater?->role)
|
||||||
|
<small class="text-body-secondary">{{ $item->updater->role->label() }}</small>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td><span class="text-body-secondary">{{ $item->updated_at?->translatedFormat('d M Y H:i') ?? '-' }}</span></td>
|
||||||
|
<td class="text-center"><span class="badge bg-label-{{ $item->status->warna() }}">{{ $item->status->label() }}</span></td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4">Belum ada data petani yang terverifikasi.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center justify-content-md-end border-top pt-4">
|
||||||
|
{{ $petaniTerverifikasi->links('pagination::bootstrap-5') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kolom-detail" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
@extends('monitoring-verifikasi.index')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$idSubTab = 'verifikasi';
|
||||||
|
$subTabs = [
|
||||||
|
'lengkap' => ['label' => 'Data Lengkap', 'ikon' => 'tabler-checklist', 'view' => 'monitoring-verifikasi.petani-lengkap', 'jumlah' => $petaniLengkap->total()],
|
||||||
|
'terverifikasi' => ['label' => 'Terverifikasi', 'ikon' => 'tabler-rosette-discount-check', 'view' => 'monitoring-verifikasi.petani-terverifikasi', 'jumlah' => $petaniTerverifikasi->total()],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const gayaTombolSwal = () => ({
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: { confirmButton: 'btn btn-primary me-2', cancelButton: 'btn btn-label-secondary' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Langkah verifikasi: 1 Input Skor → 2 Input Wajib → 3 Input Lainnya.
|
||||||
|
function tampilkanLangkah(stepper, aktif) {
|
||||||
|
stepper.querySelectorAll('.langkah-pane').forEach((pane) => {
|
||||||
|
pane.classList.toggle('d-none', Number(pane.dataset.langkah) !== aktif);
|
||||||
|
});
|
||||||
|
|
||||||
|
stepper.querySelectorAll('.langkah-item').forEach((item) => {
|
||||||
|
const n = Number(item.dataset.langkah);
|
||||||
|
const nomor = item.querySelector('.langkah-nomor');
|
||||||
|
const selesai = n < aktif;
|
||||||
|
|
||||||
|
nomor.className = 'avatar-initial rounded-circle langkah-nomor '
|
||||||
|
+ (n === aktif ? 'bg-primary text-white' : selesai ? 'bg-label-success' : 'bg-label-secondary');
|
||||||
|
nomor.innerHTML = selesai ? '<i class="icon-base ti tabler-check icon-sm"></i>' : n;
|
||||||
|
|
||||||
|
// Langkah yang sudah dilewati bisa diklik untuk kembali.
|
||||||
|
if (selesai) item.dataset.keLangkah = n; else delete item.dataset.keLangkah;
|
||||||
|
item.classList.toggle('cursor-pointer', selesai);
|
||||||
|
});
|
||||||
|
|
||||||
|
stepper.closest('.kartu-rincian-isi')?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const tombol = e.target.closest('[data-ke-langkah]');
|
||||||
|
if (!tombol) return;
|
||||||
|
const stepper = tombol.closest('.stepper-verifikasi');
|
||||||
|
if (stepper) tampilkanLangkah(stepper, Number(tombol.dataset.keLangkah));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Konfirmasi sebelum menandai Terverifikasi.
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
const form = e.target.closest('.form-verifikasi-petani');
|
||||||
|
if (!form || form.dataset.terkonfirmasi) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const kirim = () => { form.dataset.terkonfirmasi = '1'; form.submit(); };
|
||||||
|
const pesan = `Data petani ${form.dataset.nama} sudah diperiksa dan akan ditandai Terverifikasi.`;
|
||||||
|
|
||||||
|
if (!window.Swal) {
|
||||||
|
if (confirm(pesan)) kirim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'question',
|
||||||
|
title: 'Verifikasi Data Petani',
|
||||||
|
text: pesan,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Ya, Verifikasi',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
...gayaTombolSwal(),
|
||||||
|
}).then((r) => { if (r.isConfirmed) kirim(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kembalikan ke Draf (Belum Lengkap / ada temuan setelah diverifikasi): wajib isi catatan untuk enumerator.
|
||||||
|
const escapeHtml = (teks) => String(teks).replace(/[&<>"']/g, (c) => '&#' + c.charCodeAt(0) + ';');
|
||||||
|
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
const form = e.target.closest('.form-kembalikan-draf');
|
||||||
|
if (!form || form.dataset.terkonfirmasi) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const kirim = (catatan) => {
|
||||||
|
form.querySelector('input[name="catatan"]').value = catatan.trim();
|
||||||
|
form.dataset.terkonfirmasi = '1';
|
||||||
|
form.submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!window.Swal) {
|
||||||
|
const catatan = prompt('Catatan untuk enumerator (wajib):');
|
||||||
|
if (catatan && catatan.trim()) kirim(catatan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: form.dataset.judul || 'Kembalikan ke Draf',
|
||||||
|
html: `Data petani <strong>${escapeHtml(form.dataset.nama)}</strong> akan dikembalikan ke <strong>Draf</strong>.<br>${escapeHtml(form.dataset.pesan || '')}`,
|
||||||
|
input: 'textarea',
|
||||||
|
inputPlaceholder: 'Contoh: Foto KTP belum diunggah, luas lahan perlu dicek ulang...',
|
||||||
|
inputAttributes: { maxlength: 2000, rows: 4 },
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Kirim',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
inputValidator: (nilai) => !nilai?.trim() && 'Catatan wajib diisi.',
|
||||||
|
...gayaTombolSwal(),
|
||||||
|
}).then((r) => { if (r.isConfirmed) kirim(r.value); });
|
||||||
|
});
|
||||||
|
|
||||||
|
@if (session('verifikasi_success') || session('verifikasi_error') || $errors->has('catatan'))
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
const pesan = @json(session('verifikasi_success') ?? session('verifikasi_error') ?? $errors->first('catatan'));
|
||||||
|
const ikon = @json(session('verifikasi_success') ? 'success' : 'error');
|
||||||
|
window.Swal ? Swal.fire({ icon: ikon, text: pesan, ...gayaTombolSwal() }) : alert(pesan);
|
||||||
|
});
|
||||||
|
@endif
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -322,6 +322,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($petani && $petani->catatan && $petani->status === \App\Enums\StatusDataPetani::DRAFT)
|
||||||
|
{{-- Dikembalikan koordinator (Belum Lengkap): yang perlu diperbaiki enumerator --}}
|
||||||
|
<div class="card border border-warning mb-6">
|
||||||
|
<div class="card-body py-4 d-flex align-items-start">
|
||||||
|
<span class="avatar avatar-sm flex-shrink-0 me-3">
|
||||||
|
<span class="avatar-initial rounded bg-label-warning"><i class="icon-base ti tabler-message-exclamation icon-20px"></i></span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">Catatan Koordinator</h6>
|
||||||
|
<p class="mb-0" style="white-space: pre-line;">{{ $petani->catatan }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="nav-align-top">
|
<div class="nav-align-top">
|
||||||
@if(!$petani)
|
@if(!$petani)
|
||||||
<ul class="nav nav-pills flex-column flex-md-row mb-6 row-gap-2 nav-pills-custom-spacing">
|
<ul class="nav nav-pills flex-column flex-md-row mb-6 row-gap-2 nav-pills-custom-spacing">
|
||||||
@@ -404,4 +419,8 @@
|
|||||||
|
|
||||||
@if ($petani)
|
@if ($petani)
|
||||||
<x-kelengkapan-petani :petani="$petani" :lahan="$lahanSelected ?? null" />
|
<x-kelengkapan-petani :petani="$petani" :lahan="$lahanSelected ?? null" />
|
||||||
|
|
||||||
|
@if ($petani->status === \App\Enums\StatusDataPetani::TERVERIFIKASI)
|
||||||
|
<x-kunci-petani-terverifikasi />
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -359,7 +359,12 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<!-- <a href="{{ route('petani.show', $item->id) }}" class="btn btn-icon btn-text-secondary rounded-pill view-record"><i class="ti tabler-eye ti-md"></i></a> -->
|
<!-- <a href="{{ route('petani.show', $item->id) }}" class="btn btn-icon btn-text-secondary rounded-pill view-record"><i class="ti tabler-eye ti-md"></i></a> -->
|
||||||
<a href="{{ route('petani.edit', $item->id) }}" class="btn btn-icon btn-text-secondary rounded-pill edit-record"><i class="ti tabler-edit ti-md"></i></a>
|
<a href="{{ route('petani.edit', $item->id) }}" class="btn btn-icon btn-text-secondary rounded-pill edit-record"><i class="ti tabler-edit ti-md"></i></a>
|
||||||
|
@if ($item->status === \App\Enums\StatusDataPetani::TERVERIFIKASI)
|
||||||
|
{{-- Terverifikasi: tidak bisa dihapus --}}
|
||||||
|
<span class="btn btn-icon btn-text-secondary rounded-pill disabled" title="Data terverifikasi tidak dapat dihapus"><i class="ti tabler-lock ti-md"></i></span>
|
||||||
|
@else
|
||||||
<a href="javascript:;" class="btn btn-icon btn-text-secondary rounded-pill delete-record" data-id="{{ $item->id }}" data-nama="{{ $item->nama }}"><i class="ti tabler-trash ti-md"></i></a>
|
<a href="javascript:;" class="btn btn-icon btn-text-secondary rounded-pill delete-record" data-id="{{ $item->id }}" data-nama="{{ $item->nama }}"><i class="ti tabler-trash ti-md"></i></a>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -225,6 +225,23 @@ Route::middleware('auth')
|
|||||||
Route::resource('realisasi', App\Http\Controllers\Web\Program\RealisasiController::class)->only('index', 'store', 'show', 'update', 'destroy');
|
Route::resource('realisasi', App\Http\Controllers\Web\Program\RealisasiController::class)->only('index', 'store', 'show', 'update', 'destroy');
|
||||||
Route::resource('verifikasi', App\Http\Controllers\Web\Program\VerifikasiController::class)->only('store', 'destroy');
|
Route::resource('verifikasi', App\Http\Controllers\Web\Program\VerifikasiController::class)->only('store', 'destroy');
|
||||||
|
|
||||||
|
// Monitoring & Verifikasi (super_admin, admin, koordinator)
|
||||||
|
Route::prefix('monitoring-verifikasi')->name('monitoring-verifikasi.')->group(function () {
|
||||||
|
Route::get('petani/monitoring', [App\Http\Controllers\Web\Monitoring\MonitoringPetaniController::class, 'index'])->name('petani.monitoring');
|
||||||
|
Route::get('petani/monitoring/{petani}/{jenis}', [App\Http\Controllers\Web\Monitoring\MonitoringPetaniController::class, 'detail'])
|
||||||
|
->whereNumber('petani')->whereIn('jenis', ['skor', 'wajib', 'lainnya'])->name('petani.monitoring.detail');
|
||||||
|
Route::get('petani/monitoring/enumerator/{user}', [App\Http\Controllers\Web\Monitoring\MonitoringPetaniController::class, 'enumerator'])
|
||||||
|
->whereNumber('user')->name('petani.monitoring.enumerator');
|
||||||
|
|
||||||
|
Route::get('petani/verifikasi', [App\Http\Controllers\Web\Verifikasi\VerifikasiPetaniController::class, 'index'])->name('petani.verifikasi');
|
||||||
|
Route::get('petani/verifikasi/{petani}', [App\Http\Controllers\Web\Verifikasi\VerifikasiPetaniController::class, 'show'])
|
||||||
|
->whereNumber('petani')->name('petani.verifikasi.show');
|
||||||
|
Route::put('petani/verifikasi/{petani}', [App\Http\Controllers\Web\Verifikasi\VerifikasiPetaniController::class, 'update'])
|
||||||
|
->whereNumber('petani')->name('petani.verifikasi.update');
|
||||||
|
Route::put('petani/verifikasi/{petani}/kembalikan-draf', [App\Http\Controllers\Web\Verifikasi\VerifikasiPetaniController::class, 'kembalikanKeDraf'])
|
||||||
|
->whereNumber('petani')->name('petani.verifikasi.kembalikan-draf');
|
||||||
|
});
|
||||||
|
|
||||||
// Petani (dibatasi per role & komoditas koordinator, lihat BatasiAksesPetani)
|
// Petani (dibatasi per role & komoditas koordinator, lihat BatasiAksesPetani)
|
||||||
Route::middleware('akses.petani')->group(function () {
|
Route::middleware('akses.petani')->group(function () {
|
||||||
Route::get('petani/show-image/{id}/{field}', [App\Http\Controllers\Web\Produktivitas\PetaniController::class, 'showImage'])->name('petani.show-image');
|
Route::get('petani/show-image/{id}/{field}', [App\Http\Controllers\Web\Produktivitas\PetaniController::class, 'showImage'])->name('petani.show-image');
|
||||||
|
|||||||
Reference in New Issue
Block a user