Update Monitoring dan Verifikasi Petani

This commit is contained in:
2026-11-05 11:01:12 +07:00
parent fd93e0d265
commit a3ec431d49
22 changed files with 1780 additions and 76 deletions
@@ -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([
'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.accepted' => 'Centang pernyataan kelengkapan data terlebih dahulu.',
'pernyataan.accepted' => 'Setujui pernyataan kelengkapan data terlebih dahulu.',
]);
$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.");
}
}
+23 -2
View File
@@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use App\Enums\Role;
use App\Enums\StatusDataPetani;
use App\Models\Pertanian\Petani;
use Closure;
use Illuminate\Http\Request;
@@ -12,6 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
* Dipasang di semua route modul data petani (petani/{petani}/...).
* - Hanya role pengelola data petani yang boleh masuk.
* - Koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan.
* - Petani berstatus Terverifikasi hanya bisa dilihat (simpan/hapus ditolak).
*/
class BatasiAksesPetani
{
@@ -30,9 +32,18 @@ class BatasiAksesPetani
$petani = $request->route('petani');
if ($petani !== null && $user->isKoordinator()) {
$petani = $petani instanceof Petani ? $petani : Petani::find($petani);
// 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);
if ($user->isKoordinator()) {
abort_unless(
$petani && $user->bolehAksesPetani($petani),
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);
}
}