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; } }