29 Commits
Author SHA1 Message Date
Syaharani 12aa655ba4 Update BUMDes 2026-11-05 21:42:00 +07:00
Syaharani ddd07b1397 Update BUMDes 2026-11-05 21:33:10 +07:00
Syaharani 291ebf919c Update Kelompok Tani 2026-11-05 21:30:38 +07:00
Syaharani c55ff109a0 Update BUMDes 2026-11-05 19:20:05 +07:00
afriadirahman 9cdd178297 Update Spatie versi 4 2026-11-05 17:06:11 +07:00
afriadirahman b76fa9e59b Update composer json untuk spatie 2026-11-05 16:39:08 +07:00
afriadirahman 7a279ad5d2 Update Log aktivitas 2026-11-05 16:34:33 +07:00
afriadirahman a3ec431d49 Update Monitoring dan Verifikasi Petani 2026-11-05 11:01:12 +07:00
afriadirahman fd93e0d265 Update Dinamis header dan form input 2026-11-04 20:25:31 +07:00
afriadirahman 8ef4ae1f15 Update Status kelengkapan data 2026-11-04 17:31:22 +07:00
afriadirahman 89c5a1371a Update Alert Profil Petani 2026-11-04 16:20:33 +07:00
afriadirahman 41da3a71ee Update Role fasilitator, kordinator 2026-11-04 14:24:51 +07:00
afriadirahman b5b046ed86 Update Pendidikan dan Pelatihan 2026-11-04 13:17:36 +07:00
afriadirahman aa91c3fe3f Update Praktik Budidaya 2026-11-04 13:09:30 +07:00
Syaharani 53913bebe9 update kelompok tani 2026-11-03 17:53:53 +07:00
Althaf f455f8b878 Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web 2026-11-03 14:47:04 +07:00
Althaf f1112e3b68 update controller 2026-11-03 10:51:50 +07:00
afriadirahman c1370a26ca Update Harga Internasional 2026-11-03 09:37:59 +07:00
Syaharani 013cb7b728 dashboard pasar 2026-10-21 10:14:49 +07:00
Althaf 4ab745cfe5 sumber informasi harga 2026-10-19 10:03:05 +07:00
Althaf c66eed56e2 Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web 2026-10-16 15:11:49 +07:00
Syaharani 8be0906a2e Update profil petani 2026-10-16 15:48:42 +07:00
afriadirahman fcd79b7cfe Update dashboard Profil Detail Petani 2026-10-16 15:25:21 +07:00
Althaf bea9be8a39 resolve aksespasar 2026-10-16 15:11:39 +07:00
afriadirahman 5220b10de7 Update Config js 2026-10-16 10:38:30 +07:00
Syaharani 24725e3df1 update chart dashboard 2026-10-16 10:29:42 +07:00
Syaharani 60d32a88e9 update dashboard produksi 2026-10-16 10:04:56 +07:00
Althaf 4370d3c3d0 Merge branch 'main' of https://git.probindo.com/Probindo/database-pertani-web 2026-10-15 15:14:57 +07:00
Althaf 7699430301 new field akses pasar 2026-10-15 15:10:18 +07:00
272 changed files with 17965 additions and 4088 deletions
+6
View File
@@ -63,3 +63,9 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# Optional CA bundle path for Yahoo Finance when PHP has no system CA configured.
YAHOO_FINANCE_CA_BUNDLE=
ALPHA_VANTAGE_API_KEY=
+715
View File
@@ -0,0 +1,715 @@
# AISTANDAR — Standar Pengembangan Aplikasi database-pertani-web
Dokumen ini adalah **aturan wajib** bagi AI (dan developer) yang bekerja di repo ini.
Baca seluruhnya sebelum membuat, mengubah, atau merefaktor kode.
## 0. Cara memakai dokumen ini
- Aturan di sini **mengalahkan** kebiasaan umum atau contoh dari luar. Jika ada konflik antara kode lama dan dokumen ini, ikuti dokumen ini **untuk kode baru**, dan ikuti Bagian 13 (Prosedur Refaktor) untuk kode lama.
- Jika sesuatu **tidak diatur** di sini: lihat dulu bagaimana modul yang sudah sesuai standar melakukannya, lalu tiru polanya. Jika tetap tidak jelas, **tanya**, jangan mengarang pola baru.
- **Jangan** menambah package/library baru, mengubah struktur database yang sudah ada, atau mengubah kontrak API tanpa izin eksplisit.
- Bahasa: teks yang tampil ke pengguna (label, pesan, validasi) memakai **Bahasa Indonesia**. Nama domain (petani, lahan, komoditas) tetap Bahasa Indonesia; kata kerja teknis Laravel (`index`, `store`, `show`) tetap bahasa Inggris.
---
## 1. Stack
| Bagian | Teknologi |
|---|---|
| Framework | Laravel 12 |
| Frontend web | Blade (tanpa Livewire/Inertia) |
| UI template | Vuexy (Bootstrap 5), ikon Tabler |
| Grafik | ApexCharts |
| Peta | Leaflet |
| Auth web | Session |
| Auth API | Laravel Sanctum (token) |
| Dokumentasi API | dedoc/scramble |
| Role | `app/Enums/Role.php`: `super_admin`, `admin` (admin program), `koordinator` (per komoditas), `enumerator` (penginput data petani), `fasilitator` (pemateri workshop), `petani`, `offtaker` |
---
## 2. Struktur folder & penamaan
### 2.1 Pengelompokan per domain
Controller, Model, Request, dan View **dikelompokkan per domain yang sama**:
```
Domain yang ada: Dashboard, Laporan, Produktivitas, Ekosistem, Program, Master,
Bumdes, Koperasi, Konfigurasi
```
| Lapisan | Lokasi |
|---|---|
| Controller web | `app/Http/Controllers/Web/{Domain}/{Model}Controller.php` |
| Controller API | `app/Http/Controllers/Api/{Domain}/{Model}Controller.php` |
| Form Request | `app/Http/Requests/{Domain}/Store{Model}Request.php`, `Update{Model}Request.php` |
| Model | `app/Models/{DomainModel}/{Model}.php` (ikuti folder model yang ada: `Pertanian/`, `Wilayah/`, `Program/`, `Master/`, dst.) |
| Enum | `app/Enums/` |
| Service / Query | `app/Services/{Domain}/` |
| View | `resources/views/{domain}/{modul}/` (huruf kecil, kebab-case) |
Controller baru **tidak boleh** ditaruh di root `app/Http/Controllers/`. Controller root yang sudah ada (`ProgramController`, `ProgramKomoditasController`, `ProgramLokasiController`) dipindahkan ke domain `Program/` saat direfaktor.
### 2.2 Konvensi nama
| Objek | Aturan | Contoh |
|---|---|---|
| Model | Singular, PascalCase | `Petani`, `JenisKomoditas` |
| Tabel | snake_case, **selalu ditulis eksplisit** di model | `protected $table = 'petani';` |
| Controller | `{Model}Controller` | `PetaniController` |
| Route name | `{modul}.{aksi}`, kebab-case | `jenis-komoditas.index` |
| URL | kebab-case | `/jenis-komoditas` |
| View | `{domain}/{modul}/{aksi}.blade.php` | `master/provinsi/index.blade.php` |
| Method tambahan | camelCase | `byProvinsi()`, `list()` |
> Karena nama domain berbahasa Indonesia, pluralisasi otomatis Laravel salah (`petanis`). **Wajib** set `$table` di setiap model, dan set parameter route bila perlu (`->parameters(['jenis-komoditas' => 'jenisKomoditas'])`).
---
## 3. Controller
> **Cakupan:** Bagian 3 berlaku untuk **modul CRUD** (pengelolaan data: tambah, ubah, hapus).
> Controller **Dashboard** dan **Laporan/Reporting** tidak terikat aturan 7 method dan mengikuti aturan yang lebih fleksibel di **Bagian 11**.
### 3.1 Tujuh method resmi
Untuk modul CRUD, gunakan **hanya** nama method resource Laravel:
| Method | HTTP | URL | Route name | Fungsi |
|---|---|---|---|---|
| `index` | GET | `/items` | `items.index` | Daftar data |
| `create` | GET | `/items/create` | `items.create` | Form tambah |
| `store` | POST | `/items` | `items.store` | Simpan baru |
| `show` | GET | `/items/{item}` | `items.show` | Detail |
| `edit` | GET | `/items/{item}/edit` | `items.edit` | Form ubah |
| `update` | PUT/PATCH | `/items/{item}` | `items.update` | Simpan perubahan |
| `destroy` | DELETE | `/items/{item}` | `items.destroy` | Hapus (soft delete) |
**Dilarang** memakai nama `add`, `view`, `save`, `delete`, `remove`, `detail`, atau sejenisnya untuk aksi CRUD. (Larangan ini tidak berlaku untuk Dashboard dan Laporan, lihat Bagian 11.)
### 3.2 Prinsip controller tipis
- Validasi **selalu** di Form Request, tidak pernah `$request->validate()` di controller.
- Controller hanya: terima request → panggil model/service → kembalikan respons.
- Logika bisnis yang lebih dari ±15 baris, dipakai di lebih dari satu tempat, atau melibatkan banyak tabel → pindahkan ke `app/Services/{Domain}/`.
- Operasi yang menulis ke lebih dari satu tabel → bungkus dengan `DB::transaction()`.
- Pakai route model binding (`Petani $petani`), bukan `find($id)`.
### 3.3 Tiga pola resmi
Pilih **satu** pola per modul berdasarkan tabel ini:
| Pola | Kapan dipakai | Contoh |
|---|---|---|
| **A. Offcanvas (AJAX)** | Data referensi/sederhana: sedikit field, tanpa halaman detail yang kaya | Provinsi, Kabupaten, Komoditas, Varietas, Adat |
| **B. Resource halaman** | Entitas utama: banyak field, ada relasi, perlu halaman detail | Lahan, Produksi, Workshop, BUMDes, Koperasi |
| **C. Detail multi-tab** | Entitas induk dengan banyak bagian data yang diisi bertahap | Petani, Survey Kelompok Tani |
---
#### Pola A — Offcanvas (AJAX)
- `index` mengembalikan **view**; `store`, `show`, `update`, `destroy` mengembalikan **JSON**.
- Tidak ada halaman `create`/`edit`; form ada di offcanvas di `index.blade.php`.
- `show` dipakai JS untuk mengisi form edit di offcanvas.
```php
// routes/web.php
// Route tambahan WAJIB didaftarkan SEBELUM Route::resource agar tidak tertangkap {provinsi}
Route::get('provinsi/list', [ProvinsiController::class, 'list'])->name('provinsi.list');
Route::resource('provinsi', ProvinsiController::class)->except(['create', 'edit']);
```
```php
namespace App\Http\Controllers\Web\Master;
class ProvinsiController extends Controller
{
public function index(Request $request)
{
$provinsi = Provinsi::query()
->when($request->search, fn ($q, $s) => $q->where('nama', 'like', "%{$s}%"))
->latest()
->paginate(15)
->withQueryString();
return view('master.provinsi.index', compact('provinsi'));
}
public function store(StoreProvinsiRequest $request)
{
$provinsi = Provinsi::create($request->validated());
return response()->json([
'success' => true,
'message' => 'Provinsi berhasil ditambahkan.',
'data' => $provinsi,
], 201);
}
public function show(Provinsi $provinsi)
{
return response()->json(['success' => true, 'data' => $provinsi]);
}
public function update(UpdateProvinsiRequest $request, Provinsi $provinsi)
{
$provinsi->update($request->validated());
return response()->json([
'success' => true,
'message' => 'Provinsi berhasil diperbarui.',
'data' => $provinsi,
]);
}
public function destroy(Provinsi $provinsi)
{
DB::transaction(fn () => $provinsi->delete());
return response()->json([
'success' => true,
'message' => 'Provinsi berhasil dihapus.',
]);
}
}
```
**Dropdown berantai** (provinsi → kabupaten → kecamatan → desa): method `by{Induk}` di controller anak, mengembalikan JSON `[{id, nama}]`.
```php
Route::get('kabupaten/by-provinsi/{provinsi}', [KabupatenController::class, 'byProvinsi'])
->name('kabupaten.by-provinsi');
```
---
#### Pola B — Resource halaman
- Semua 7 method; view `index`, `create`, `edit`, `show` + partial `_form`.
- Setelah `store`/`update`/`destroy` → **redirect** dengan flash message.
```php
Route::resource('lahan', LahanController::class);
```
```php
namespace App\Http\Controllers\Web\Produktivitas;
class LahanController extends Controller
{
public function index(Request $request)
{
$lahan = Lahan::query()
->with(['petani', 'desa'])
->when($request->search, fn ($q, $s) => $q->where('nama', 'like', "%{$s}%"))
->latest()
->paginate(15)
->withQueryString();
return view('produktivitas.lahan.index', compact('lahan'));
}
public function create()
{
return view('produktivitas.lahan.create', ['lahan' => new Lahan()]);
}
public function store(StoreLahanRequest $request)
{
$lahan = Lahan::create($request->validated());
return redirect()->route('lahan.show', $lahan)
->with('success', 'Lahan berhasil ditambahkan.');
}
public function show(Lahan $lahan)
{
$lahan->load(['petani', 'tanaman']);
return view('produktivitas.lahan.show', compact('lahan'));
}
public function edit(Lahan $lahan)
{
return view('produktivitas.lahan.edit', compact('lahan'));
}
public function update(UpdateLahanRequest $request, Lahan $lahan)
{
$lahan->update($request->validated());
return redirect()->route('lahan.show', $lahan)
->with('success', 'Lahan berhasil diperbarui.');
}
public function destroy(Lahan $lahan)
{
DB::transaction(fn () => $lahan->delete());
return redirect()->route('lahan.index')
->with('success', 'Lahan berhasil dihapus.');
}
}
```
---
#### Pola C — Detail multi-tab
- Controller induk (mis. `PetaniController`) memakai Pola B.
- Setiap tab punya **controller sendiri** (`PetaniLahanController`, `PraktikBudidayaController`, …) yang tetap memakai nama method resmi.
- Tab dirangkai sebagai **nested shallow resource** di bawah induk.
- View: `header.blade.php` berisi kartu profil + navigasi tab; setiap tab punya subfolder.
- Tab selain Profil **terkunci** sampai profil induk lengkap. Logika penguncian ditaruh di satu tempat (method di model induk, mis. `$petani->profilLengkap()`), bukan diulang di tiap view.
```
resources/views/produktivitas/petani/
├── index.blade.php
├── create.blade.php
├── edit.blade.php
├── show.blade.php ← tab Profil
├── header.blade.php ← kartu profil + navigasi tab
├── _form.blade.php
├── lahan-tanaman/
│ ├── index.blade.php
│ └── ...
├── praktik-budidaya/
└── ...
```
```php
Route::resource('petani', PetaniController::class);
Route::resource('petani.praktik-budidaya', PraktikBudidayaController::class)->shallow();
```
### 3.4 Batas resource
- Jika modul tidak butuh semua aksi, batasi dengan `->only([...])` / `->except([...])`. Jangan biarkan route yang tidak punya method terdaftar.
- Method di luar 7 aksi resmi hanya boleh untuk: dropdown/list (`list`, `by{Induk}`), aksi status yang jelas (`verify`, `approve`), dan export (`export`). Daftarkan sebelum `Route::resource`.
---
## 4. Validasi
- Setiap `store` dan `update` memakai Form Request: `Store{Model}Request`, `Update{Model}Request`.
- Pesan dan atribut dalam Bahasa Indonesia (`messages()` / `attributes()`, atau file `lang/id/validation.php`).
- Form Request yang sama boleh dipakai controller Web dan API.
- Untuk Pola A, Form Request otomatis mengembalikan JSON 422 karena request AJAX mengirim header `Accept: application/json`. **Jangan** menangkap error validasi secara manual.
- **Unique wajib mengabaikan data yang sudah di-soft delete** (lihat 5.3).
```php
public function rules(): array
{
return [
'kode' => [
'required', 'string', 'max:10',
Rule::unique('provinsi', 'kode')
->ignore($this->route('provinsi'))
->withoutTrashed(),
],
'nama' => ['required', 'string', 'max:100'],
];
}
```
---
## 5. Model, migration, dan soft delete
### 5.1 Model
```php
class Lahan extends Model
{
use SoftDeletes;
protected $table = 'lahan';
protected $fillable = ['petani_id', 'desa_id', 'nama', 'luas'];
protected function casts(): array
{
return [
'luas' => 'decimal:2',
'status_milik' => StatusKepemilikan::class, // Enum
];
}
}
```
- Pakai `$fillable`, bukan `$guarded = []`.
- Pilihan tetap (jawaban survei, status) disimpan sebagai **PHP Enum** di `app/Enums/` dan di-cast di model. Jangan menulis daftar pilihan langsung di Blade.
- **Jejak aktivitas:** setiap model memakai trait `App\Models\Concerns\CatatAktivitas` (spatie/laravel-activitylog). Tambah/ubah/hapus otomatis tercatat di `activity_log` dan tampil di menu Konfigurasi > Log Aktivitas. Kolom audit, `$hidden`, file/foto, dan polygon (`path`) tidak dicatat. Jika domain model baru belum ada, tambahkan labelnya di `LogAktivitasController::JENIS`. Aksi bisnis yang bukan sekadar simpan model (verifikasi, ganti status) dicatat manual dengan `activity('nama_log')->performedOn($model)->log('...')`. Perubahan lewat query massal (`->update()` pada query builder) **tidak** tercatat.
### 5.2 Migration
Setiap tabel baru **wajib** memiliki:
```php
$table->id();
// ... kolom
$table->foreignId('created_by')->nullable()->constrained('users'); // ikuti kolom audit yang sudah ada
$table->foreignId('updated_by')->nullable()->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->foreignId('deleted_by')->nullable()->constrained('users');
```
- Jangan mengubah migration yang sudah dijalankan di production; buat migration baru.
- **Jangan** memakai unique index di database untuk kolom pada tabel ber-soft delete (data terhapus akan menghalangi input ulang). Keunikan dijaga di Form Request.
### 5.3 Soft delete (aturan utama)
1. **Semua penghapusan adalah soft delete.** `forceDelete()` dilarang di kode aplikasi.
2. **`deleted_by` wajib terisi** dengan user yang menghapus. Gunakan mekanisme yang sudah ada di project (trait/observer). Jika belum ada, gunakan trait berikut dan pasang di semua model:
```php
namespace App\Models\Concerns;
trait HasDeletedBy
{
protected static function bootHasDeletedBy(): void
{
static::deleting(function ($model) {
if (! $model->isForceDeleting()) {
$model->deleted_by = auth()->id();
$model->saveQuietly();
}
});
}
}
```
3. **Hapus berantai (cascade soft delete).** Jika induk dihapus, **semua data anaknya ikut di-soft delete**, termasuk cucu dan seterusnya. Tulis di model induk:
```php
protected static function booted(): void
{
static::deleting(function (Petani $petani) {
if ($petani->isForceDeleting()) {
return;
}
// ->each->delete() memicu event per anak, sehingga deleted_by
// terisi dan cascade berlanjut ke cucu
$petani->lahan()->each->delete();
$petani->praktikBudidaya()->each->delete();
// ... semua relasi hasMany/hasOne milik petani
});
}
```
- **Jangan** memakai `$petani->lahan()->delete()` (query massal), karena tidak memicu event, sehingga `deleted_by` kosong dan cascade terputus.
- Setiap kali menambah relasi anak baru, **tambahkan juga** ke blok `deleting` induknya.
- Pemanggilan `delete()` di controller selalu dibungkus `DB::transaction()`.
- Relasi `belongsTo` ke data master (Provinsi, Komoditas, dll.) **bukan** anak dan tidak ikut terhapus.
4. **Restore** belum disediakan di aplikasi; dilakukan langsung di database oleh admin. Jangan membuat halaman sampah/restore tanpa diminta.
### 5.4 Global scope
- `KomoditasProgramScope` otomatis membatasi data ke komoditas milik program aktif (`SystemProgram`). Jika data "tidak muncul", periksa scope ini dulu.
- Melewati scope hanya dengan `withoutGlobalScope(KomoditasProgramScope::class)` secara eksplisit, dengan komentar alasannya.
- Jangan menambah global scope baru tanpa izin.
---
## 6. Routing
### 6.1 Route CRUD wajib memakai `Route::resource`
Semua modul CRUD (Pola A, B, C) **wajib** didaftarkan dengan `Route::resource`. **Dilarang** menulis route CRUD satu per satu secara manual.
```php
// ❌ SALAH — route CRUD manual
Route::get('provinsi', [ProvinsiController::class, 'index'])->name('provinsi.index');
Route::post('provinsi/store', [ProvinsiController::class, 'store'])->name('provinsi.store');
Route::get('provinsi/view/{id}', [ProvinsiController::class, 'view'])->name('provinsi.view');
Route::post('provinsi/update/{id}', [ProvinsiController::class, 'update'])->name('provinsi.update');
Route::get('provinsi/delete/{id}', [ProvinsiController::class, 'destroy'])->name('provinsi.destroy');
// ✅ BENAR
Route::resource('provinsi', ProvinsiController::class)->except(['create', 'edit']);
```
Aturan turunannya:
| Kebutuhan | Cara |
|---|---|
| Tidak semua aksi dipakai | `->only([...])` atau `->except([...])` |
| Pola A (offcanvas) | `->except(['create', 'edit'])` |
| Pola B (resource halaman) | `Route::resource(...)` penuh |
| Pola C (multi-tab) | Induk `Route::resource`, tab `Route::resource('induk.tab', ...)->shallow()` |
| Beberapa modul sekaligus | `Route::resources([...])` boleh dipakai dalam satu grup domain |
| Nama parameter tidak sesuai model | `->parameters(['jenis-komoditas' => 'jenisKomoditas'])` |
| Method tambahan (`list`, `by{Induk}`, `verify`, `export`) | `Route::get/post/patch` biasa, **ditulis sebelum** `Route::resource` modul yang sama |
- Form hapus/ubah memakai method spoofing Blade (`@method('DELETE')`, `@method('PUT')`), dan AJAX mengirim method HTTP yang sesuai. Jangan memakai `GET` untuk menghapus.
- **Pengecualian:** Dashboard dan Laporan tidak memakai `Route::resource` (lihat Bagian 11). Halaman tunggal seperti Profil akun boleh memakai `Route::singleton('profil', ...)` atau route manual `edit`/`update`.
### 6.2 Aturan routing lainnya
- Semua route web di `routes/web.php`, API di `routes/api.php`.
- Kelompokkan per domain dengan `Route::middleware(...)->group()` dan komentar judul domain.
- Nested resource memakai `->shallow()`. Maksimal **dua tingkat** di URL (`/framework/{framework}/transformasi`). Hierarki lebih dalam (Framework → Transformasi → Aktivitas → Target) dipecah menjadi beberapa nested shallow resource berpasangan.
- **Satu route hanya didaftarkan satu kali.** Sebelum menambah route, cari dulu apakah sudah ada (`php artisan route:list --name=...`).
- Setelah login: `enumerator` → Dashboard Survei Petani; `fasilitator` → Profil akun (belum ada menu khusus); role lain → Dashboard Karakteristik Petani. Jangan mengubah logika ini tanpa izin.
---
## 7. Blade & Vuexy
### 7.1 Layout
- Semua halaman admin: `@extends('layout.main')` dan mengisi `@section('content')`.
- Halaman auth memakai `auth/layout.blade.php`.
- Jangan membuat layout baru; jangan menyalin `<head>`/sidebar ke halaman.
### 7.2 File view per pola
| File | Pola | Isi |
|---|---|---|
| `index.blade.php` | A, B, C | Tabel, pencarian, pagination (+ offcanvas form untuk Pola A) |
| `create.blade.php` | B, C | Form tambah, `@include('..._form')` |
| `edit.blade.php` | B, C | Form ubah, `@include('..._form')` |
| `show.blade.php` | B, C | Detail |
| `_form.blade.php` | A, B, C | Field form bersama untuk tambah dan ubah |
| `header.blade.php` | C | Kartu profil + navigasi tab |
- File `add.blade.php` dan `view.blade.php` **tidak boleh** dibuat lagi (ganti dengan `create` dan `show`).
- `_form.blade.php` menerima model (kosong saat tambah) dan memakai `old('field', $model->field)`.
### 7.3 Tampilan error validasi
```blade
<input type="text" name="nama"
class="form-control @error('nama') is-invalid @enderror"
value="{{ old('nama', $lahan->nama) }}">
@error('nama')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
```
Untuk Pola A, JS menampilkan error dari respons 422 dengan class yang sama (`is-invalid` + `invalid-feedback`).
### 7.4 Komponen
- Elemen yang muncul di lebih dari dua halaman dijadikan Blade component di `resources/views/components/` (contoh yang ada: `summary-card`, `wilayah-card`, `wilayah-dashboard`).
- Utamakan komponen yang sudah ada sebelum membuat yang baru.
### 7.5 JavaScript & plugin
- JS halaman ditulis di **file terpisah per modul** (ikuti lokasi JS yang sudah dipakai project), dimuat lewat section/stack script di layout. Hindari blok `<script>` panjang di Blade.
- Semua request AJAX mengirim header `X-CSRF-TOKEN` dan `Accept: application/json`.
- URL di JS diambil dari `route()` yang dicetak Blade (mis. atribut `data-url`), **bukan** di-hardcode.
- Pakai plugin yang sudah tersedia di template Vuexy. Jangan menambah library baru tanpa izin. Pembagian peran:
| Kebutuhan | Plugin |
|---|---|
| Konfirmasi hapus | SweetAlert2 (jika terpasang) |
| Notifikasi sukses/gagal | Sesuai yang dipakai modul lain |
| Select dengan pencarian / dropdown berantai | Select2 (jika terpasang) |
| Input tanggal | Flatpickr (jika terpasang) |
| Grafik | ApexCharts |
| Peta | Leaflet |
| Ikon | Tabler Icons (`ti ti-*`) |
---
## 8. Respons & pesan
### 8.1 Web (Pola B/C)
```php
return redirect()->route('lahan.index')->with('success', 'Lahan berhasil dihapus.');
return back()->withInput()->with('error', 'Data gagal disimpan.');
```
Kunci flash: `success`, `error`, `warning`, `info`.
### 8.2 JSON (Pola A dan AJAX web)
```json
{ "success": true, "message": "Provinsi berhasil ditambahkan.", "data": { } }
```
- Error validasi: format 422 bawaan Laravel (`message` + `errors`).
- Error lain: `{ "success": false, "message": "..." }` dengan kode HTTP yang sesuai.
### 8.3 Pola kalimat pesan
`{Nama data} berhasil ditambahkan.` / `diperbarui.` / `dihapus.`
---
## 9. Otorisasi
- Hak akses berdasarkan role pada `Role` yang sudah ada. Ikuti mekanisme pembatasan role yang sudah dipakai (middleware/policy/gate yang ada); jangan membuat sistem permission baru.
- Penginput data petani adalah role `enumerator` (bukan `fasilitator`). `fasilitator` khusus pemateri workshop.
- Semua route `petani/...` dibungkus middleware `akses.petani` (`BatasiAksesPetani`): hanya `super_admin`, `admin`, `koordinator`, `enumerator`; koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan (`koordinator_komoditas`, cek lewat `User::bolehAksesPetani()`). Route tab petani baru wajib diletakkan di dalam grup ini.
- Menu sidebar hanya menampilkan item yang boleh diakses role pengguna, dan route-nya juga dilindungi di sisi server. Menyembunyikan menu saja tidak cukup.
---
## 10. API (`app/Http/Controllers/Api/`)
- API dipakai aplikasi mobile/lapangan. **Kontrak API (URL, nama field, format respons) tidak boleh diubah** tanpa izin eksplisit, termasuk saat refaktor.
- Endpoint baru mengikuti pola dan format respons API yang sudah ada di folder `Api/`.
- Auth memakai Sanctum; login dibatasi rate limit (5/menit). Jangan longgarkan.
- Setiap endpoint baru harus terbaca rapi di dokumentasi Scramble (type-hint Form Request dan tipe return yang jelas).
---
## 11. Pengecualian
### 11.0 Prinsip umum Dashboard & Laporan
Dashboard dan laporan **bersifat baca (read-only)** dan bentuknya beragam, sehingga controller-nya **fleksibel**:
- **Nama method bebas**, asalkan deskriptif dan camelCase (`index`, `chartProduktivitas`, `petaSebaran`, `rekapPerDesa`, `exportExcel`).
- Boleh satu controller dengan banyak method, atau controller satu aksi (`__invoke`) jika halamannya sederhana.
- Route ditulis manual (`Route::get(...)`), **tidak** memakai `Route::resource`.
- Tidak ada `create/store/edit/update/destroy`. Jika sebuah dashboard butuh input data, input itu dibuat sebagai modul CRUD terpisah.
Yang **tetap berlaku** walau fleksibel:
- Lokasi folder per domain (Bagian 2), penamaan route kebab-case dengan prefix (`dashboard.*`, `laporan.*`).
- Validasi filter memakai Form Request jika filternya lebih dari 2–3 parameter.
- Query sederhana boleh di controller. Query agregat yang panjang, berat, atau dipakai di lebih dari satu tempat dipindah ke `app/Services/{Dashboard|Laporan}/`.
- Data yang ditampilkan tetap menghormati soft delete (data terhapus tidak ikut dihitung) dan global scope program.
- Pembatasan akses per role (Bagian 9).
### 11.1 Dashboard
- Controller: `app/Http/Controllers/Web/Dashboard/{Nama}Controller.php`.
- Pola yang disarankan: `index` mengembalikan halaman; data grafik/peta yang dimuat via AJAX memakai method bernama jelas yang mengembalikan JSON.
- Filter umum diterima lewat query string: program, wilayah (provinsi → desa), periode.
- Query berat di-cache (`Cache::remember`) dengan kunci yang memuat nilai filter.
- View: satu file per dashboard di `resources/views/dashboard/`. Grafik ApexCharts, peta Leaflet.
- Data eksternal (AlphaVantage di `InternationalMarketController`) wajib di-cache dan punya penanganan gagal (tampilkan pesan, jangan error 500).
```php
// routes/web.php
Route::prefix('dashboard')->name('dashboard.')->group(function () {
Route::get('produktivitas', [ProduktivitasController::class, 'index'])->name('produktivitas');
Route::get('produktivitas/chart-panen', [ProduktivitasController::class, 'chartPanen'])
->name('produktivitas.chart-panen');
});
```
### 11.2 Laporan / reporting
- Controller: `app/Http/Controllers/Web/Laporan/{Nama}Controller.php`; view di `resources/views/laporan/{nama}/`.
- Pola yang disarankan per laporan: halaman filter + pratinjau (`index`), lalu method export per format (`exportExcel`, `exportPdf`). Semua menerima **filter yang sama** dengan pratinjau.
- Export kecil yang menempel pada satu modul CRUD atau dashboard boleh berupa method `export` di controller modul tersebut.
- Laporan yang lambat (data besar) boleh diproses di queue, lalu file diunduh setelah siap.
- Gunakan package export yang sudah terpasang di project. Jika belum ada, tanyakan dulu.
```php
Route::prefix('laporan')->name('laporan.')->group(function () {
Route::get('rekap-panen', [RekapPanenController::class, 'index'])->name('rekap-panen');
Route::get('rekap-panen/excel', [RekapPanenController::class, 'exportExcel'])->name('rekap-panen.excel');
Route::get('rekap-panen/pdf', [RekapPanenController::class, 'exportPdf'])->name('rekap-panen.pdf');
});
```
### 11.3 Ekosistem (form builder dinamis)
- Modul `Ekosistem/` (Group, Tabel, Kolom, Formulir, Ringkasan, Value) memiliki struktur dinamis dan **dikecualikan** dari pola CRUD standar.
- Jangan direfaktor ke pola standar kecuali diminta secara eksplisit. Perubahan kecil tetap mengikuti aturan validasi, soft delete, dan kebersihan.
---
## 12. Kebersihan repo
- **Dilarang** menyimpan file salinan/cadangan: `* copy.php`, `*Backup*`, `*01.php`, `*_old*`, dll. Riwayat disimpan di Git.
- Tidak ada kode yang dikomentari dalam jumlah besar ("kode mati"); hapus saja.
- Tidak ada route ganda (lihat Bagian 6).
- Satu commit = satu perubahan logis (satu modul/fitur). Pesan commit Bahasa Indonesia, singkat, diawali jenis: `feat:`, `fix:`, `refactor:`, `chore:`.
Masalah kebersihan yang **sudah diketahui** (selesaikan saat menyentuh area terkait):
- File duplikat/backup: `* copy.php`, `IndikatorPertanianBackupController`, `PanenController01`, `SummaryDashboardController copy.php`, file `composerlock` yang tidak semestinya.
- Route ganda: `users` (resource + manual), `users.verify` (dua kali), `jenis-komoditas.by-komoditas`, `varietas.by-jenis-komoditas`.
---
## 13. Prosedur refaktor modul lama
Modul lama **wajib** disesuaikan dengan standar ini secara bertahap.
### 13.1 Aturan
1. **Satu modul per sesi/commit.** Jangan merefaktor beberapa modul sekaligus.
2. Jangan merefaktor modul yang sedang dikerjakan (ada perubahan belum di-commit) tanpa konfirmasi.
3. API **tidak** ikut diubah (Bagian 10).
4. Perilaku untuk pengguna harus tetap sama; refaktor bukan tempat menambah fitur.
### 13.2 Langkah
1. Simpan daftar route modul: `php artisan route:list --name={modul} > /tmp/before.txt`.
2. Tentukan pola target (A/B/C) sesuai 3.3.
3. Cari **semua** pemakaian nama route, URL, dan view lama di seluruh repo (Blade, JS, controller lain, sidebar), mis. `grep -rn "provinsi.view\|provinsi/add\|master.provinsi.add" app resources routes`.
4. Ubah:
- method `add` → `create`, `view` → `show`;
- file `add.blade.php` → `create.blade.php`, `view.blade.php` → `show.blade.php`;
- route manual → `Route::resource` (+ `only/except`, route tambahan di atasnya);
- validasi inline → Form Request (dengan `withoutTrashed()` pada unique);
- pastikan model memakai `SoftDeletes`, `deleted_by` terisi, dan cascade ke anak sudah ditulis;
- semua referensi yang ditemukan di langkah 3.
5. Bandingkan `route:list` sesudah dengan `/tmp/before.txt`; pastikan tidak ada route yang hilang tanpa sengaja.
6. Uji manual alur lengkap: index → tambah → detail → ubah → hapus (cek `deleted_at`, `deleted_by`, dan data anak).
7. Perbarui tabel status di 13.3.
### 13.3 Status migrasi modul
Status: `Belum` / `Proses` / `Selesai` / `Dikecualikan`
| Domain | Modul | Pola target | Status | Catatan |
|---|---|---|---|---|
| Master | Provinsi | A | Belum | Ada `list` + dropdown berantai |
| Master | Kabupaten/Kota | A | Belum | `by-provinsi` |
| Master | Kecamatan | A | Belum | `by-kabupaten` |
| Master | Desa/Kelurahan | A | Belum | `by-kecamatan` |
| Master | Komoditas | A | Belum | |
| Master | Jenis Komoditas | A | Belum | Route `by-komoditas` dobel |
| Master | Varietas | A | Belum | Route `by-jenis-komoditas` dobel |
| Master | Adat | A | Belum | |
| Master | Keluarga | ? | Belum | Tentukan pola saat refaktor |
| Produktivitas | Petani (+ semua tab) | C | Belum | |
| Produktivitas | Lahan | B | Belum | |
| Produktivitas | Tanaman | B | Belum | |
| Produktivitas | MapTanaman | ? | Belum | |
| Produktivitas | Produksi | B | Belum | |
| Produktivitas | Survey Kelompok Tani (+ tab) | C | Proses | Daftar: Form Request filter, service query, dan JS terpisah; ringkasan profil disiapkan service; form/tab lama belum direfaktor |
| Produktivitas | Sebaran Petani | ? | Belum | Sedang dikerjakan, konfirmasi dulu |
| Program | Program, Program Komoditas, Program Lokasi | B | Belum | Pindah dari root ke `Program/` |
| Program | Framework → Transformasi → Aktivitas → Target | B | Belum | Nested shallow |
| Program | Realisasi | B | Belum | |
| Program | Verifikasi | ? | Belum | Sedang dikerjakan, konfirmasi dulu |
| Program | Workshop (+ Sesi, Peserta) | B | Belum | Sesi/Peserta sedang dikerjakan |
| Program | Kelompok Belajar Tani | B | Belum | |
| Bumdes | Profil, Kelembagaan, Survei | B | Belum | |
| Koperasi | Profil, Kelembagaan, Survei | B | Belum | |
| Konfigurasi | User | B | Belum | Route `users` & `users.verify` dobel |
| Konfigurasi | Profil akun | — | Belum | Hanya `edit`/`update` |
| Ekosistem | Semua | — | Dikecualikan | Lihat 11.3 |
| Dashboard | Semua | — | Dikecualikan | Ikuti 11.1 |
---
## 14. Checklist modul baru
- [ ] Pola ditentukan (A/B/C) sesuai 3.3
- [ ] Migration: `id`, kolom audit, `timestamps`, `softDeletes`, `deleted_by`; tanpa unique index
- [ ] Model: `$table` eksplisit, `$fillable`, `SoftDeletes`, `deleted_by` terisi, casts/Enum, trait `CatatAktivitas`
- [ ] Relasi anak ditambahkan ke cascade `deleting` di model induk
- [ ] Form Request `Store`/`Update` dengan pesan Bahasa Indonesia dan unique `withoutTrashed()`
- [ ] Controller di `Web/{Domain}/` dengan nama method resmi saja
- [ ] Route: `Route::resource` (+ `only/except`, `shallow`), tidak dobel, route tambahan di atas resource
- [ ] View di `{domain}/{modul}/`: `index`, `create`, `edit`, `show`, `_form` sesuai pola
- [ ] JS di file terpisah, URL dari `route()`, CSRF terpasang
- [ ] Menu sidebar ditambahkan dan dibatasi per role; route dilindungi di server
- [ ] Respons/flash mengikuti Bagian 8
- [ ] Diuji manual: tambah, ubah, detail, hapus (cek `deleted_at`, `deleted_by`, data anak)
- [ ] Tidak ada file salinan/backup tertinggal
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Enums\Bumdes;
enum AsetProduktifBumdes: string
{
case BANGUNAN_KANTOR = 'bangunan_kantor';
case GUDANG_PENYIMPANAN = 'gudang_penyimpanan';
case KENDARAAN_OPERASIONAL_ANGKUT = 'kendaraan_operasional_angkut';
case LAHAN_USAHA = 'lahan_usaha';
case ALAT_PENGOLAHAN_KOMODITAS = 'alat_pengolahan_komoditas';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::BANGUNAN_KANTOR => 'Bangunan / kantor',
self::GUDANG_PENYIMPANAN => 'Gudang penyimpanan',
self::KENDARAAN_OPERASIONAL_ANGKUT => 'Kendaraan operasional/angkut',
self::LAHAN_USAHA => 'Lahan usaha',
self::ALAT_PENGOLAHAN_KOMODITAS => 'Alat pengolahan komoditas',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum BentukProdukBumdes: string
{
case BUAH_SEGAR = 'buah_segar';
case BIJI_BASAH = 'biji_basah';
case BIJI_KERING = 'biji_kering';
case TERFERMENTASI = 'terfermentasi';
public function label(): string
{
return match ($this) {
self::BUAH_SEGAR => 'Buah segar hasil petik',
self::BIJI_BASAH => 'Biji basah',
self::BIJI_KERING => 'Biji kering/biji kupas',
self::TERFERMENTASI => 'Terfermentasi',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Enums\Bumdes;
enum CakupanLayananBumdes: string
{
case DESA_SENDIRI = 'desa_sendiri';
case DESA_LAIN = 'desa_sendiri_dan_lain';
public function label(): string
{
return match ($this) {
self::DESA_SENDIRI => 'Hanya melayani desa sendiri',
self::DESA_LAIN => 'Melayani desa sendiri dan desa lain',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Enums\Bumdes;
enum DukunganDesaBumdes: string
{
case PENDAMPINGAN_TEKNIS = 'pendampingan_teknis';
case REGULASI_PERDES = 'regulasi_perdes';
case FASILITASI_JEJARING = 'fasilitasi_jejaring';
case TIDAK_ADA = 'tidak_ada';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::PENDAMPINGAN_TEKNIS => 'Pendampingan teknis',
self::REGULASI_PERDES => 'Regulasi/Perdes pendukung',
self::FASILITASI_JEJARING => 'Fasilitasi jejaring/kemitraan',
self::TIDAK_ADA => 'Tidak ada dukungan khusus',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum FormatPembukuanBumdes: string
{
case MANUAL = 'manual';
case DIGITAL_EXCEL = 'digital_excel';
case APLIKASI = 'aplikasi';
case TIDAK_ADA = 'tidak_ada';
public function label(): string
{
return match ($this) {
self::MANUAL => 'Manual/buku kas',
self::DIGITAL_EXCEL => 'Digital/file Excel',
self::APLIKASI => 'Aplikasi keuangan khusus',
self::TIDAK_ADA => 'Tidak ada',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Enums\Bumdes;
enum HambatanUsahaBumdes: string
{
case MODAL = 'modal';
case SDM = 'sdm';
case INFRASTRUKTUR = 'infrastruktur';
case PASOKAN = 'pasokan';
case PERSAINGAN = 'persaingan';
case AKSES_PASAR = 'akses_pasar';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::MODAL => 'Keterbatasan modal',
self::SDM => 'Keterbatasan SDM/kapasitas pengurus',
self::INFRASTRUKTUR => 'Keterbatasan infrastruktur/alat',
self::PASOKAN => 'Kesulitan mendapat pasokan konsisten dari produsen',
self::PERSAINGAN => 'Persaingan dengan tengkulak/pedagang',
self::AKSES_PASAR => 'Belum ada akses ke offtaker/pasar premium',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Enums\Bumdes;
enum HubunganPemasokBumdes: string
{
case BEBAS = 'bebas';
case LANGGANAN = 'langganan';
case PINJAMAN = 'pinjaman';
case KONSINYASI = 'konsinyasi';
case KOMITMEN_VOLUME = 'komitmen_volume';
case SEMI_KONTRAK = 'semi_kontrak';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::BEBAS => 'Bebas/tidak terikat: petani bebas menjual ke BUMDes atau pihak lain kapan saja, tanpa ikatan apa pun',
self::LANGGANAN => 'Langganan tetap: petani rutin menjual ke BUMDes tanpa perjanjian formal',
self::PINJAMAN => 'Berbasis hutang/pinjaman: petani menjual ke BUMDes karena memiliki pinjaman kepada BUMDes',
self::KONSINYASI => 'Titip jual (konsinyasi): petani dibayar setelah hasil panen terjual',
self::KOMITMEN_VOLUME => 'Ada komitmen volume: kesepakatan volume tertentu yang dipenuhi secara berkala',
self::SEMI_KONTRAK => 'Semi-kontrak: kesepakatan tertulis/lisan mengenai harga, volume, dan waktu penyerahan',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Enums\Bumdes;
enum InfrastrukturPascapanenBumdes: string
{
case GUDANG_PENYIMPANAN_HASIL_PANEN = 'gudang_penyimpanan_hasil_panen';
case ALAT_MESIN_PENGERING = 'alat_mesin_pengering';
case MESIN_PULPER_HULLER = 'mesin_pulper_huller';
case TIMBANGAN = 'timbangan';
case ALAT_SORTASI = 'alat_sortasi';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::GUDANG_PENYIMPANAN_HASIL_PANEN => 'Gudang penyimpanan hasil panen',
self::ALAT_MESIN_PENGERING => 'Alat/mesin pengering',
self::MESIN_PULPER_HULLER => 'Mesin pulper/huller',
self::TIMBANGAN => 'Timbangan',
self::ALAT_SORTASI => 'Alat sortasi',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Enums\Bumdes;
enum JabatanPengurusIntiBumdes: string
{
case PENASIHAT_KEPALA_DESA = 'penasihat_kepala_desa';
case KETUA_PENGAWAS = 'ketua_pengawas';
case DIREKTUR_KETUA_BUMDES = 'direktur_ketua_bumdes';
case SEKRETARIS = 'sekretaris';
case BENDAHARA = 'bendahara';
case KEPALA_UNIT_USAHA_KOMODITAS = 'kepala_unit_usaha_komoditas';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::PENASIHAT_KEPALA_DESA => 'Penasihat (Kepala Desa)',
self::KETUA_PENGAWAS => 'Ketua Pengawas',
self::DIREKTUR_KETUA_BUMDES => 'Direktur/Ketua BUMDes',
self::SEKRETARIS => 'Sekretaris',
self::BENDAHARA => 'Bendahara',
self::KEPALA_UNIT_USAHA_KOMODITAS => 'Kepala Unit Usaha Komoditas',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Enums\Bumdes;
enum KemitraanEksternalBumdes: string
{
case NGO = 'ngo';
case PEMERINTAH_KABUPATEN = 'pemerintah_kabupaten';
case SWASTA_OFFTAKER = 'swasta_offtaker';
case PROGRAM_PEMERINTAH_LAIN = 'program_pemerintah_lain';
case BELUM_ADA = 'belum_ada';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::NGO => 'NGO/lembaga pemberdayaan',
self::PEMERINTAH_KABUPATEN => 'Dinas/pemerintah kabupaten',
self::SWASTA_OFFTAKER => 'Perusahaan swasta/offtaker',
self::PROGRAM_PEMERINTAH_LAIN => 'Program pemerintah lain',
self::BELUM_ADA => 'Belum ada kemitraan eksternal',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum KeterlibatanKoordinasiBumdes: string
{
case SELALU = 'selalu';
case KADANG = 'kadang';
case TIDAK_PERNAH = 'tidak_pernah';
public function label(): string
{
return match ($this) {
self::SELALU => 'Selalu dilibatkan',
self::KADANG => 'Kadang dilibatkan',
self::TIDAK_PERNAH => 'Tidak pernah dilibatkan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Enums\Bumdes;
enum KondisiAsetProduktifBumdes: string
{
case BERFUNGSI = 'berfungsi';
case PERLU_PERBAIKAN = 'perlu_perbaikan_evaluasi';
case RUSAK = 'rusak_tidak_bisa_dipakai';
case MANGKRAK = 'mangkrak';
public function label(): string
{
return match ($this) {
self::BERFUNGSI => 'Berfungsi',
self::PERLU_PERBAIKAN => 'Perlu perbaikan/evaluasi',
self::RUSAK => 'Rusak/tidak bisa dipakai',
self::MANGKRAK => 'Mangkrak',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
public static function normalizeLegacy(mixed $value): ?string
{
if (! is_scalar($value) || trim((string) $value) === '') {
return null;
}
$value = trim((string) $value);
return match (mb_strtolower($value)) {
'baik', 'berfungsi', 'baik dan berfungsi', 'baik_berfungsi' => self::BERFUNGSI->value,
'rusak ringan', 'perlu perbaikan', 'perlu perbaikan/evaluasi' => self::PERLU_PERBAIKAN->value,
'rusak', 'rusak/tidak terpakai', 'rusak_tidak_terpakai', 'rusak/tidak bisa dipakai' => self::RUSAK->value,
'mangkrak' => self::MANGKRAK->value,
default => $value,
};
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Enums\Bumdes;
enum KondisiInfrastrukturBumdes: string
{
case BAIK_BERFUNGSI = 'baik_berfungsi';
case RUSAK_TIDAK_TERPAKAI = 'rusak_tidak_terpakai';
public function label(): string
{
return match ($this) {
self::BAIK_BERFUNGSI => 'Baik dan berfungsi',
self::RUSAK_TIDAK_TERPAKAI => 'Rusak/tidak terpakai',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum KondisiKeuanganBumdes: string
{
case UNTUNG_SURPLUS = 'untung_surplus';
case IMPAS = 'impas';
case RUGI_DEFISIT = 'rugi_defisit';
case TIDAK_MENJAWAB = 'tidak_menjawab';
public function label(): string
{
return match ($this) {
self::UNTUNG_SURPLUS => 'Untung/surplus',
self::IMPAS => 'Impas',
self::RUGI_DEFISIT => 'Rugi/defisit',
self::TIDAK_MENJAWAB => 'Tidak tahu/tidak mau menjawab',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum KontribusiPadesBumdes: string
{
case RUTIN = 'rutin';
case TIDAK_RUTIN = 'tidak_rutin';
case BELUM_PERNAH = 'belum_pernah';
public function label(): string
{
return match ($this) {
self::RUTIN => 'Ya, rutin',
self::TIDAK_RUTIN => 'Ya, tapi tidak rutin',
self::BELUM_PERNAH => 'Belum pernah',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Enums\Bumdes;
enum LayananPetaniBumdes: string
{
case SIMPAN_PINJAM = 'simpan_pinjam';
case PENGADAAN_INPUT = 'pengadaan_input';
case PEMASARAN_PANEN = 'pemasaran_panen';
case PENYEWAAN_ALAT = 'penyewaan_alat';
case JASA_PENGOLAHAN = 'jasa_pengolahan';
case PELATIHAN = 'pelatihan';
case BELUM_ADA = 'belum_ada';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::SIMPAN_PINJAM => 'Simpan pinjam',
self::PENGADAAN_INPUT => 'Pengadaan input (pupuk, bibit, dll)',
self::PEMASARAN_PANEN => 'Pembelian/pemasaran hasil panen',
self::PENYEWAAN_ALAT => 'Penyewaan alat pengolahan',
self::JASA_PENGOLAHAN => 'Jasa pengolahan/fermentasi',
self::PELATIHAN => 'Pelatihan/pendampingan',
self::BELUM_ADA => 'Belum ada layanan terkait komoditas ini',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Enums\Bumdes;
enum MekanismePembayaranBumdes: string
{
case TUNAI = 'tunai';
case TEMPO = 'tempo';
case CAMPURAN = 'campuran';
case IJON = 'ijon';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::TUNAI => 'Tunai langsung',
self::TEMPO => 'Sistem tempo/dibayar kemudian',
self::CAMPURAN => 'Campuran, tergantung situasi',
self::IJON => 'BUMDes membayar di muka sebelum panen, sebagai bagian dari ikatan pinjaman kepada petani (ijon)',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum ModelFermentasiBumdes: string
{
case INDIVIDUAL = 'individual';
case KOLEKTIF_KELOMPOK = 'kolektif_kelompok';
case UNIT_BUMDES = 'unit_bumdes';
case CAMPURAN = 'campuran';
public function label(): string
{
return match ($this) {
self::INDIVIDUAL => 'Individual (masing-masing petani)',
self::KOLEKTIF_KELOMPOK => 'Kolektif kelompok tani',
self::UNIT_BUMDES => 'Unit pengolah BUMDes/desa',
self::CAMPURAN => 'Campuran',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum PencatatanPetaniBumdes: string
{
case RAPI = 'rapi';
case TIDAK_RAPI = 'tidak_rapi';
case TIDAK_ADA = 'tidak_ada';
public function label(): string
{
return match ($this) {
self::RAPI => 'Ada, tercatat rapi',
self::TIDAK_RAPI => 'Ada tapi tidak rapi',
self::TIDAK_ADA => 'Tidak ada pencatatan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum PenetapanHargaBeliBumdes: string
{
case PASAR_SETEMPAT = 'pasar_setempat';
case KALKULASI_BIAYA = 'kalkulasi_biaya';
case HARGA_MITRA = 'harga_mitra';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::PASAR_SETEMPAT => 'Mengikuti harga pasar/tengkulak setempat',
self::KALKULASI_BIAYA => 'Ditetapkan sendiri berdasarkan kalkulasi biaya',
self::HARGA_MITRA => 'Mengikuti harga dari offtaker/mitra',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum PengalamanOfftakerBumdes: string
{
case BERJALAN = 'berjalan';
case BERHENTI = 'berhenti';
case BELUM_PERNAH = 'belum_pernah';
public function label(): string
{
return match ($this) {
self::BERJALAN => 'Sedang berjalan',
self::BERHENTI => 'Pernah, tapi sudah berhenti',
self::BELUM_PERNAH => 'Belum pernah',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum PeranRantaiNilaiBumdes: string
{
case MEMBELI_LANGSUNG = 'membeli_langsung';
case PERANTARA = 'perantara';
case MENGOLAH_SENDIRI = 'mengolah_sendiri';
case BELUM_BERPERAN = 'belum_berperan';
public function label(): string
{
return match ($this) {
self::MEMBELI_LANGSUNG => 'Membeli langsung dari petani',
self::PERANTARA => 'Menjadi perantara/mengumpulkan untuk dijual ke pihak lain',
self::MENGOLAH_SENDIRI => 'Mengolah sendiri (sortasi/fermentasi/dll) sebelum dijual',
self::BELUM_BERPERAN => 'Belum berperan dalam rantai nilai komoditas ini',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum PeriodePanenBumdes: string
{
case TERAKHIR = 'panen_terakhir';
case TAHUN_LALU = 'tahun_lalu';
case DUA_TAHUN_LALU = 'dua_tahun_lalu';
public function label(): string
{
return match ($this) {
self::TERAKHIR => 'Panen Terakhir',
self::TAHUN_LALU => 'Tahun Lalu',
self::DUA_TAHUN_LALU => 'Dua Tahun Lalu',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum PerjanjianPetaniBumdes: string
{
case TERTULIS = 'tertulis';
case LISAN = 'lisan';
case TIDAK_ADA = 'tidak_ada';
public function label(): string
{
return match ($this) {
self::TERTULIS => 'Ada, tertulis',
self::LISAN => 'Ada, kesepakatan lisan saja',
self::TIDAK_ADA => 'Tidak ada perjanjian',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum PertanggungjawabanMusdesBumdes: string
{
case TAHUNAN = 'tahunan';
case TIDAK_RUTIN = 'tidak_rutin';
case BELUM_PERNAH = 'belum_pernah';
public function label(): string
{
return match ($this) {
self::TAHUNAN => 'Ya, setiap tahun',
self::TIDAK_RUTIN => 'Tidak rutin',
self::BELUM_PERNAH => 'Belum pernah',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Enums\Bumdes;
enum SaluranPenjualanBumdes: string
{
case OFFTAKER_EKSPORTIR = 'offtaker_eksportir';
case PEDAGANG_KABUPATEN = 'pedagang_kabupaten';
case KOPERASI_BUMDES = 'koperasi_bumdes';
case PASAR_LOKAL = 'pasar_lokal';
case BELUM_MENJUAL = 'belum_menjual';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::OFFTAKER_EKSPORTIR => 'Offtaker/eksportir langsung',
self::PEDAGANG_KABUPATEN => 'Pedagang besar/pengepul kabupaten',
self::KOPERASI_BUMDES => 'Koperasi atau BUMDes lain',
self::PASAR_LOKAL => 'Dijual eceran/pasar lokal',
self::BELUM_MENJUAL => 'Belum pernah menjual (masih disimpan/diproses)',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Enums\Bumdes;
enum SifatAuditBumdes: string
{
case INTERNAL = 'internal';
case EKSTERNAL = 'eksternal';
public function label(): string
{
return match ($this) {
self::INTERNAL => 'Internal (pengurus/pengawas BUMDes sendiri)',
self::EKSTERNAL => 'Eksternal (Inspektorat, Dinas PMD, akuntan publik/auditor independen)',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum StatusAdArtBumdes: string
{
case ADA_LENGKAP = 'ada_lengkap';
case ADA_TIDAK_LENGKAP = 'ada_tidak_lengkap';
case TIDAK_ADA = 'tidak_ada';
case TIDAK_TAHU = 'tidak_tahu';
public function label(): string
{
return match ($this) {
self::ADA_LENGKAP => 'Ada, lengkap',
self::ADA_TIDAK_LENGKAP => 'Ada tapi tidak lengkap',
self::TIDAK_ADA => 'Tidak ada',
self::TIDAK_TAHU => 'Tidak tahu',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum StatusBadanHukumBumdes: string
{
case SUDAH_BERBADAN_HUKUM = 'sudah_berbadan_hukum';
case PROSES_PENDAFTARAN = 'proses_pendaftaran';
case BELUM_BERBADAN_HUKUM = 'belum_berbadan_hukum';
case TIDAK_TAHU = 'tidak_tahu';
public function label(): string
{
return match ($this) {
self::SUDAH_BERBADAN_HUKUM => 'Sudah berbadan hukum (terdaftar Kemenkumham)',
self::PROSES_PENDAFTARAN => 'Proses pendaftaran',
self::BELUM_BERBADAN_HUKUM => 'Belum berbadan hukum',
self::TIDAK_TAHU => 'Tidak tahu',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum StatusFermentasiBumdes: string
{
case RUTIN = 'rutin';
case PERNAH_MENCOBA = 'pernah_mencoba';
case DIRENCANAKAN = 'direncanakan';
case TIDAK_DIRENCANAKAN = 'tidak_direncanakan';
public function label(): string
{
return match ($this) {
self::RUTIN => 'Ya, rutin',
self::PERNAH_MENCOBA => 'Pernah mencoba',
self::DIRENCANAKAN => 'Belum pernah, direncanakan',
self::TIDAK_DIRENCANAKAN => 'Belum pernah, tidak direncanakan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Enums\Bumdes;
enum StatusKeaktifanBumdes: string
{
case AKTIF_PENUH = 'aktif_penuh';
case AKTIF_TERBATAS = 'aktif_terbatas';
case TIDAK_AKTIF_VAKUM = 'tidak_aktif_vakum';
case BERDIRI_BELUM_BEROPERASI = 'berdiri_belum_beroperasi';
public function label(): string
{
return match ($this) {
self::AKTIF_PENUH => 'Aktif beroperasi penuh',
self::AKTIF_TERBATAS => 'Aktif tapi terbatas (sebagian unit usaha berjalan)',
self::TIDAK_AKTIF_VAKUM => 'Tidak aktif/vakum',
self::BERDIRI_BELUM_BEROPERASI => 'Sudah berdiri tapi belum pernah beroperasi',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum StatusKepemilikanAsetBumdes: string
{
case MILIK_BUMDES_SENDIRI = 'milik_bumdes_sendiri';
case MILIK_DESA_DIPINJAMKAN_DIKELOLA = 'milik_desa_dipinjamkan_dikelola';
case SEWA_PINJAM_PIHAK_LAIN = 'sewa_pinjam_pihak_lain';
public function label(): string
{
return match ($this) {
self::MILIK_BUMDES_SENDIRI => 'Milik BUMDes sendiri',
self::MILIK_DESA_DIPINJAMKAN_DIKELOLA => 'Milik desa (dipinjamkan/dikelola)',
self::SEWA_PINJAM_PIHAK_LAIN => 'Sewa/pinjam pihak lain',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum StatusPembukuanBumdes: string
{
case RUTIN_TERTIB = 'rutin_tertib';
case TIDAK_RUTIN = 'tidak_rutin';
case TIDAK_ADA = 'tidak_ada';
public function label(): string
{
return match ($this) {
self::RUTIN_TERTIB => 'Ya, rutin dan tertib',
self::TIDAK_RUTIN => 'Ada tapi tidak rutin/tidak rapi',
self::TIDAK_ADA => 'Tidak ada pembukuan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum StatusPengeringanBumdes: string
{
case RUTIN = 'rutin';
case KADANG = 'kadang';
case TIDAK_PERNAH = 'tidak_pernah';
public function label(): string
{
return match ($this) {
self::RUTIN => 'Ya, rutin',
self::KADANG => 'Kadang-kadang',
self::TIDAK_PERNAH => 'Tidak pernah, produk dijual dalam kondisi diterima',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum StatusSortasiBumdes: string
{
case RUTIN_STANDAR = 'rutin_standar';
case TIDAK_KONSISTEN = 'tidak_konsisten';
case BELUM_PERNAH = 'belum_pernah';
public function label(): string
{
return match ($this) {
self::RUTIN_STANDAR => 'Ya, rutin dengan standar tertentu',
self::TIDAK_KONSISTEN => 'Ya, tapi tidak konsisten',
self::BELUM_PERNAH => 'Belum pernah dilakukan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Enums\Bumdes;
enum StatusUnitKomoditasBumdes: string
{
case YA = 'ya';
case DITANGANI_PENGURUS_INTI = 'ditangani_pengurus_inti';
case DIRENCANAKAN = 'direncanakan';
public function label(): string
{
return match ($this) {
self::YA => 'Ya, ada unit khusus',
self::DITANGANI_PENGURUS_INTI => 'Tidak ada unit khusus, ditangani langsung pengurus inti',
self::DIRENCANAKAN => 'Belum ada, masih direncanakan',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Enums\Bumdes;
enum SumberPasokanBumdes: string
{
case ANGGOTA_KELOMPOK = 'anggota_kelompok';
case PETANI_NON_ANGGOTA = 'petani_non_anggota';
case PENGUMPUL_DESA = 'pengumpul_desa';
case CAMPURAN = 'campuran';
case LAINNYA = 'lainnya';
public function label(): string
{
return match ($this) {
self::ANGGOTA_KELOMPOK => 'Anggota kelompok tani langsung',
self::PETANI_NON_ANGGOTA => 'Petani non-anggota',
self::PENGUMPUL_DESA => 'Pengumpul/sub-agen desa',
self::CAMPURAN => 'Campuran',
self::LAINNYA => 'Lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Enums\Bumdes;
enum UnitUsahaBumdes: string
{
case SIMPAN_PINJAM = 'simpan_pinjam';
case PERDAGANGAN_TOKO_DESA = 'perdagangan_toko_desa';
case PENGELOLAAN_AIR_BERSIH = 'pengelolaan_air_bersih';
case WISATA_DESA_AGROWISATA = 'wisata_desa_agrowisata';
case PENGELOLAAN_HASIL_PERTANIAN_KOMODITAS = 'pengelolaan_hasil_pertanian_komoditas';
case PENYEWAAN_ALAT_ASET = 'penyewaan_alat_aset';
case JASA_LAINNYA = 'jasa_lainnya';
public function label(): string
{
return match ($this) {
self::SIMPAN_PINJAM => 'Simpan pinjam',
self::PERDAGANGAN_TOKO_DESA => 'Perdagangan/toko desa',
self::PENGELOLAAN_AIR_BERSIH => 'Pengelolaan air bersih',
self::WISATA_DESA_AGROWISATA => 'Wisata desa/agrowisata',
self::PENGELOLAAN_HASIL_PERTANIAN_KOMODITAS => 'Pengelolaan hasil pertanian/komoditas',
self::PENYEWAAN_ALAT_ASET => 'Penyewaan alat/aset',
self::JASA_LAINNYA => 'Jasa lainnya',
};
}
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $value) => [$value->value => $value->label()])->all();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Enums;
enum JenisKelompokTani: string
{
case POKTAN = 'Kelompok Tani (Poktan)';
case KWT = 'Kelompok Wanita Tani (KWT)';
case GAPOKTAN = 'Gabungan Kelompok Tani (Gapoktan)';
case PEMUDA_TANI = 'Kelompok Pemuda Tani';
case ASOSIASI = 'Asosiasi Petani';
case LAINNYA = 'Lainnya';
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $jenis) => [$jenis->value => $jenis->value])->all();
}
}
@@ -4,12 +4,13 @@ namespace App\Enums\PendidikanPelatihan;
enum PelatihanGAP: string
{
case PEMANGKASAN = 'Pemangkasan atau perawatan tanaman';
case PEMUPUKAN = 'Pemupukan atau nutrisi tanah';
case PENGENDALIAN = 'Pengendalian hama dan penyakit';
case PEMANGKASAN = 'Pemangkasan/perawatan tanaman';
case PEMUPUKAN = 'Pemupukan/nutrisi';
case PENGENDALIAN = 'Pengendalian hama & penyakit';
case PRAKTIK = 'Praktik Berkebun yang Baik (GAP)';
case PENGOLAHAN = 'Pengolahan/pascapanen';
case PENYERBUKAN = 'Penyerbukan manual (Vanili)';
case PENYERBUKAN = 'Penyerbukan Manual (Vanili)';
case LAINNYA = 'Lainnya';
public function label(): string
{
+16 -1
View File
@@ -6,7 +6,22 @@ enum Role: string
{
case SUPER_ADMIN = 'super_admin';
case ADMIN = 'admin'; // admin program
case FASILITATOR = 'fasilitator';
case KOORDINATOR = 'koordinator'; // koordinator per komoditas
case ENUMERATOR = 'enumerator'; // penginput data petani di lapangan
case FASILITATOR = 'fasilitator'; // pemateri workshop
case PETANI = 'petani';
case OFFTAKER = 'offtaker';
public function label(): string
{
return match ($this) {
self::SUPER_ADMIN => 'Super Admin',
self::ADMIN => 'Admin',
self::KOORDINATOR => 'Koordinator',
self::ENUMERATOR => 'Enumerator',
self::FASILITATOR => 'Fasilitator',
self::PETANI => 'Petani',
self::OFFTAKER => 'Offtaker',
};
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Enums;
// Status kelengkapan data profil petani.
// Draf → Lengkap (ditandai enumerator penginput) → Terverifikasi (oleh koordinator, menu menyusul).
enum StatusDataPetani: string
{
case DRAFT = 'draft';
case LENGKAP = 'lengkap';
case TERVERIFIKASI = 'terverifikasi';
public function label(): string
{
return match ($this) {
self::DRAFT => 'Draf',
self::LENGKAP => 'Lengkap',
self::TERVERIFIKASI => 'Terverifikasi',
};
}
// Warna badge Vuexy (bg-label-{warna})
public function warna(): string
{
return match ($this) {
self::DRAFT => 'secondary',
self::LENGKAP => 'info',
self::TERVERIFIKASI => 'success',
};
}
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Enums;
enum StatusLegalitasKelompokTani: string
{
case BELUM_ADA = 'Belum ada SK/dokumen apa pun';
case PROSES = 'Sedang proses pengajuan';
case SK_DESA_KECAMATAN = 'Sudah ada SK dari desa/kecamatan';
case SK_DINAS_KABUPATEN = 'Sudah ada SK dan terdaftar di Dinas/Kabupaten';
case TIDAK_TAHU = 'Tidak tahu';
public static function options(): array
{
return collect(self::cases())->mapWithKeys(fn (self $status) => [$status->value => $status->value])->all();
}
}
@@ -20,7 +20,7 @@ class FormBasisDataController extends Controller
*/
public function index(Request $request, string $tabelId): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -20,7 +20,7 @@ class TabelBasisDataController extends Controller
*/
public function index(Request $request): AnonymousResourceCollection
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -50,7 +50,7 @@ class TabelBasisDataController extends Controller
*/
public function show(Request $request, string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -19,7 +19,7 @@ class ValueBasisDataController extends Controller
public function index(Request $request, string $tabelId): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -213,7 +213,7 @@ class ValueBasisDataController extends Controller
public function store(Request $request, string $tabelId): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -278,7 +278,7 @@ class ValueBasisDataController extends Controller
public function update(Request $request, string $tabelId, string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -354,7 +354,7 @@ class ValueBasisDataController extends Controller
public function destroy(Request $request, string $tabelId, string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -20,7 +20,7 @@ class JenisKomoditasController extends Controller
*/
public function index(Request $request): AnonymousResourceCollection
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -63,7 +63,7 @@ class JenisKomoditasController extends Controller
*/
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -85,7 +85,7 @@ class JenisKomoditasController extends Controller
*/
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -107,7 +107,7 @@ class JenisKomoditasController extends Controller
*/
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -129,7 +129,7 @@ class JenisKomoditasController extends Controller
*/
public function destroy(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -13,7 +13,7 @@ class DistributionController extends Controller
{
public function index(): JsonResponse
{
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
abort(403);
}
@@ -35,7 +35,7 @@ class DistributionController extends Controller
public function store(Request $request): JsonResponse
{
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
abort(403);
}
@@ -60,7 +60,7 @@ class DistributionController extends Controller
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
abort(403);
}
@@ -86,7 +86,7 @@ class DistributionController extends Controller
public function batchUpsert(Request $request): JsonResponse
{
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
abort(403);
}
@@ -21,7 +21,7 @@ class LahanController extends Controller
*/
public function index(Request $request): AnonymousResourceCollection
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -77,7 +77,7 @@ class LahanController extends Controller
*/
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -91,7 +91,7 @@ class LahanController extends Controller
$request->merge(['profile_id' => (int) $request->profile_id]);
}
$profileRule = $user->role === 'fasilitator' ? ['required', 'numeric', 'exists:profiles,id'] : ['nullable'];
$profileRule = $user->role === 'enumerator' ? ['required', 'numeric', 'exists:profiles,id'] : ['nullable'];
$validated = $request->validate([
'nama' => ['required', 'string', 'max:255'],
@@ -105,11 +105,11 @@ class LahanController extends Controller
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'], // Max 5MB
]);
$targetProfileId = $user->role === 'fasilitator'
$targetProfileId = $user->role === 'enumerator'
? $validated['profile_id']
: $profile->id;
if ($user->role === 'fasilitator') {
if ($user->role === 'enumerator') {
$targetProfile = Profile::findOrFail($targetProfileId);
if ($targetProfile->desa_kelurahan_id !== $profile->desa_kelurahan_id) {
@@ -117,7 +117,7 @@ class LahanController extends Controller
}
}
$desaKelurahanId = $user->role === 'fasilitator'
$desaKelurahanId = $user->role === 'enumerator'
? $profile->desa_kelurahan_id
: ($validated['desa_kelurahan_id'] ?? $profile->desa_kelurahan_id);
@@ -149,14 +149,14 @@ class LahanController extends Controller
*/
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$lahan = Lahan::with('desaKelurahan')->findOrFail($id);
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
return response()->json(['message' => 'Unauthorized'], 403);
} else if (Auth::user()->role === 'fasilitator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
} else if (Auth::user()->role === 'enumerator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
@@ -168,14 +168,14 @@ class LahanController extends Controller
*/
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$lahan = Lahan::findOrFail($id);
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
return response()->json(['message' => 'Unauthorized'], 403);
} else if (Auth::user()->role === 'fasilitator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
} else if (Auth::user()->role === 'enumerator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
@@ -215,14 +215,14 @@ class LahanController extends Controller
*/
public function destroy(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$lahan = Lahan::findOrFail($id);
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
return response()->json(['message' => 'Unauthorized'], 403);
} else if (Auth::user()->role === 'fasilitator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
} else if (Auth::user()->role === 'enumerator' && $lahan->desa_kelurahan_id !== Auth::user()->profile->desa_kelurahan_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
@@ -233,7 +233,7 @@ class LahanController extends Controller
public function batchUpsert(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -12,7 +12,7 @@ class OfftakerController extends Controller
{
public function index(Request $request): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
@@ -29,7 +29,7 @@ class OfftakerController extends Controller
public function store(Request $request): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
@@ -58,7 +58,7 @@ class OfftakerController extends Controller
public function show(string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
$offtaker = Offtaker::findOrFail($id);
@@ -67,7 +67,7 @@ class OfftakerController extends Controller
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
$offtaker = Offtaker::findOrFail($id);
@@ -87,7 +87,7 @@ class OfftakerController extends Controller
public function destroy(string $id): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
Offtaker::findOrFail($id)->delete();
@@ -100,7 +100,7 @@ class OfftakerController extends Controller
*/
public function batchUpsert(Request $request): JsonResponse
{
if (!Gate::any(['super_admin', 'petani', 'fasilitator', 'admin','offtaker'])) {
if (!Gate::any(['super_admin', 'petani', 'enumerator', 'admin','offtaker'])) {
abort(403, 'Unauthorized');
}
@@ -15,7 +15,7 @@ class PanenController extends Controller
{
public function index(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -30,7 +30,7 @@ class PanenController extends Controller
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -87,7 +87,7 @@ class PanenController extends Controller
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$panen = Panen::findOrFail($id);
@@ -96,7 +96,7 @@ class PanenController extends Controller
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$panen = Panen::findOrFail($id);
@@ -114,7 +114,7 @@ class PanenController extends Controller
public function destroy(string $id): JsonResponse
{
Gate::authorize(['petani', 'fasilitator']);
Gate::authorize(['petani', 'enumerator']);
Panen::findOrFail($id)->delete();
return response()->json(null, 204);
}
@@ -124,7 +124,7 @@ class PanenController extends Controller
*/
public function batchUpsert(Request $request): JsonResponse
{
if (Gate::authorize(['petani', 'fasilitator','super_admin'])) {
if (Gate::authorize(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$profileId = Auth::user()->profile->id;
@@ -14,7 +14,7 @@ class PanenController01 extends Controller
{
public function index(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -29,7 +29,7 @@ class PanenController01 extends Controller
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -58,7 +58,7 @@ class PanenController01 extends Controller
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$panen = Panen::findOrFail($id);
@@ -67,7 +67,7 @@ class PanenController01 extends Controller
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$panen = Panen::findOrFail($id);
@@ -85,7 +85,7 @@ class PanenController01 extends Controller
public function destroy(string $id): JsonResponse
{
Gate::authorize(['petani', 'fasilitator']);
Gate::authorize(['petani', 'enumerator']);
Panen::findOrFail($id)->delete();
return response()->json(null, 204);
}
@@ -95,7 +95,7 @@ class PanenController01 extends Controller
*/
public function batchUpsert(Request $request): JsonResponse
{
if (Gate::authorize(['petani', 'fasilitator','super_admin'])) {
if (Gate::authorize(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$profileId = Auth::user()->profile->id;
@@ -23,7 +23,7 @@ class ProfileController extends Controller
*/
public function index(Request $request)
{
if(!Gate::any(['fasilitator', 'super_admin'])) {
if(!Gate::any(['enumerator', 'super_admin'])) {
abort(403);
}
@@ -34,7 +34,7 @@ class ProfileController extends Controller
$profileId = $user->role === 'petani' ? $profile->id : $requestID;
$desaKelurahanId = null;
if ($profile->role === 'fasilitator' || $profile->role === 'super_admin') {
if ($profile->role === 'enumerator' || $profile->role === 'super_admin') {
$desaKelurahanId = $profile->desa_kelurahan_id;
}
@@ -95,7 +95,7 @@ class ProfileController extends Controller
*/
public function store(Request $request)
{
if(!Gate::any(['fasilitator','super_admin'])){
if(!Gate::any(['enumerator','super_admin'])){
abort(403);
}
@@ -139,7 +139,7 @@ class ProfileController extends Controller
public function show(string $id): JSONResponse
{
// attempt to access profile, only for allowed roles
if (!Gate::any(['petani', 'fasilitator', 'offtaker', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'offtaker', 'super_admin'])) {
abort(403);
}
@@ -166,7 +166,7 @@ class ProfileController extends Controller
*/
public function update(Request $request, string $id): JSONResponse
{
if (!Gate::any(['petani', 'fasilitator', 'offtaker', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'offtaker', 'super_admin'])) {
abort(403);
}
@@ -177,7 +177,7 @@ class ProfileController extends Controller
$profile = Profile::findOrFail($id);
if ($user->role === 'fasilitator' && $profile->desa_kelurahan_id != $user->profile->desa_kelurahan_id) {
if ($user->role === 'enumerator' && $profile->desa_kelurahan_id != $user->profile->desa_kelurahan_id) {
abort(403, 'Unauthorized');
}
@@ -239,7 +239,7 @@ class ProfileController extends Controller
public function downloadFile(string $id, string $type): JsonResponse | BinaryFileResponse
{
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
abort(403);
}
@@ -260,7 +260,7 @@ class ProfileController extends Controller
public function verify(Request $request, string $id): JSONResponse
{
if (!Gate::any(['fasilitator'])) {
if (!Gate::any(['enumerator'])) {
abort(403);
}
@@ -22,7 +22,7 @@ class TanamanController extends Controller
*/
public function index(Request $request): AnonymousResourceCollection
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$user = Auth::user();
@@ -91,7 +91,7 @@ class TanamanController extends Controller
*/
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -135,7 +135,7 @@ class TanamanController extends Controller
*/
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -160,7 +160,7 @@ class TanamanController extends Controller
*/
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$tanaman = Tanaman::findOrFail($id);
@@ -195,7 +195,7 @@ class TanamanController extends Controller
*/
public function destroy(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$tanaman = Tanaman::findOrFail($id);
@@ -22,7 +22,7 @@ class TanamanController extends Controller
*/
public function index(Request $request): AnonymousResourceCollection
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$user = Auth::user();
@@ -91,7 +91,7 @@ class TanamanController extends Controller
*/
public function store(Request $request): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -141,7 +141,7 @@ class TanamanController extends Controller
*/
public function show(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
@@ -166,7 +166,7 @@ class TanamanController extends Controller
*/
public function update(Request $request, string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$tanaman = Tanaman::findOrFail($id);
@@ -201,7 +201,7 @@ class TanamanController extends Controller
*/
public function destroy(string $id): JsonResponse
{
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
abort(403);
}
$tanaman = Tanaman::findOrFail($id);
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Web\Bumdes;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bumdes\BumdesRequest;
use App\Http\Requests\Bumdes\StoreKelembagaanAktorBumdesRequest;
use App\Models\Bumdes\PengurusBumdes;
use App\Services\Bumdes\PengurusBumdesService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class KelembagaanAktorBumdesController extends Controller
{
public function __construct(private readonly PengurusBumdesService $service) {}
public function index(BumdesRequest $request, PengurusBumdes $pengurusBumdes): View
{
return view('bumdes.kelembagaan-aktor.index', $this->service->sectionData($pengurusBumdes));
}
public function store(StoreKelembagaanAktorBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
$this->service->saveSection($pengurusBumdes, $request->validated(), 'kelembagaan');
return redirect()->route('bumdes.kelembagaan-aktor.index', $pengurusBumdes)
->with('success', 'Data Kelembagaan berhasil disimpan.');
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Web\Bumdes;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bumdes\BumdesRequest;
use App\Http\Requests\Bumdes\StoreOperasionalBumdesRequest;
use App\Models\Bumdes\PengurusBumdes;
use App\Services\Bumdes\PengurusBumdesService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class OperasionalBumdesController extends Controller
{
public function __construct(private readonly PengurusBumdesService $service) {}
public function index(BumdesRequest $request, PengurusBumdes $pengurusBumdes): View
{
return view('bumdes.operasional.index', $this->service->sectionData($pengurusBumdes));
}
public function store(StoreOperasionalBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
$this->service->saveSection($pengurusBumdes, $request->validated(), 'operasional');
return redirect()->route('bumdes.operasional.index', $pengurusBumdes)
->with('success', 'Data Operasional berhasil disimpan.');
}
}
@@ -2,579 +2,66 @@
namespace App\Http\Controllers\Web\Bumdes;
use App\Enums\Bumdes\JabatanBumdes;
use App\Enums\Pendidikan;
use App\Http\Controllers\Controller;
use App\Models\Bumdes\KelembagaanAktorBumdes;
use App\Http\Requests\Bumdes\BumdesRequest;
use App\Http\Requests\Bumdes\IndexPengurusBumdesRequest;
use App\Http\Requests\Bumdes\StorePengurusBumdesRequest;
use App\Http\Requests\Bumdes\UpdatePengurusBumdesRequest;
use App\Models\Bumdes\PengurusBumdes;
use App\Models\Wilayah\Provinsi;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Services\Bumdes\PengurusBumdesService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class PengurusBumdesController extends Controller
{
public function index(Request $request)
public function __construct(private readonly PengurusBumdesService $service) {}
public function index(IndexPengurusBumdesRequest $request): View
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$query = PengurusBumdes::with([
...$this->wilayahRelationsWithoutProgramScope(),
'creator:id,name',
])
->orderBy('created_at', 'desc');
if ($request->search) {
$query->where(function ($query) use ($request) {
$query->where('nama', 'like', '%' . $request->search . '%')
->orWhere('jabatan', 'like', '%' . $request->search . '%')
->orWhere('jabatan_lainnya', 'like', '%' . $request->search . '%');
});
}
if (auth()->user()->role->value === 'fasilitator') {
$query->where('created_by', auth()->id());
}
$summaryQuery = clone $query;
$totalPengurus = (clone $summaryQuery)->count();
$totalLakiLaki = (clone $summaryQuery)->where('jenis_kelamin', true)->count();
$totalPerempuan = (clone $summaryQuery)->where('jenis_kelamin', false)->count();
$totalDesaKelurahan = (clone $summaryQuery)->distinct('desa_kelurahan_id')->count('desa_kelurahan_id');
$totalJabatan = (clone $summaryQuery)->distinct('jabatan')->count('jabatan');
$pengurus = $query->paginate($request->per_page ?? 10);
$userRole = auth()->user()->role->value;
$showEnumerator = in_array($userRole, ['super_admin', 'admin']);
return view('bumdes.profil.index', compact(
'pengurus',
'totalPengurus',
'totalLakiLaki',
'totalPerempuan',
'totalDesaKelurahan',
'totalJabatan',
'showEnumerator'
));
return view('bumdes.profil.index', $this->service->indexData($request->validated()));
}
public function create()
public function create(BumdesRequest $request): View
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$provinsi = $this->provinsiOptions();
$jabatan = JabatanBumdes::options();
$pendidikan = Pendidikan::options();
return view('bumdes.profil.add', compact('provinsi', 'jabatan', 'pendidikan'));
return view('bumdes.profil.create', $this->service->profileData());
}
public function store(Request $request)
public function store(StorePengurusBumdesRequest $request): RedirectResponse
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$pengurus = $this->service->storeProfile($request->validated());
$validatedData = $this->validatedData($request);
$pengurus = PengurusBumdes::create([
...$this->formatData($validatedData),
'uid' => 'BMD-' . strtoupper(Str::random(10)),
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
return redirect()->route('bumdes.edit', $pengurus->id)->with('success', 'Data pengurus BUMDes berhasil ditambahkan.');
return $this->profileSavedResponse($request, $pengurus);
}
public function show(string $id)
public function show(BumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
return redirect()->route('bumdes.edit', $id);
return redirect()->route('bumdes.edit', $pengurusBumdes);
}
public function edit(string $id)
public function edit(BumdesRequest $request, PengurusBumdes $pengurusBumdes): View
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$pengurus = $this->pengurusQuery()
->with($this->wilayahRelationsWithoutProgramScope())
->findOrFail($id);
$provinsi = $this->provinsiOptions();
$jabatan = JabatanBumdes::options();
$pendidikan = Pendidikan::options();
return view('bumdes.profil.edit', compact('pengurus', 'provinsi', 'jabatan', 'pendidikan'));
return view('bumdes.profil.edit', $this->service->profileData($pengurusBumdes));
}
public function update(Request $request, string $id)
public function update(UpdatePengurusBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$this->service->updateProfile($pengurusBumdes, $request->validated());
$pengurus = $this->pengurusQuery()->findOrFail($id);
$validatedData = $this->validatedData($request);
$pengurus->update([
...$this->formatData($validatedData),
'updated_by' => auth()->id(),
]);
return redirect()->route('bumdes.edit', $pengurus->id)->with('success', 'Data pengurus BUMDes berhasil diupdate.');
return $this->profileSavedResponse($request, $pengurusBumdes);
}
public function destroy(string $id)
public function destroy(BumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
DB::transaction(fn () => $pengurusBumdes->delete());
$pengurus = $this->pengurusQuery()->findOrFail($id);
$pengurus->update(['deleted_by' => auth()->id()]);
$pengurus->delete();
return response()->json(['message' => 'Data pengurus BUMDes berhasil dihapus.']);
return redirect()->route('bumdes.index')->with('success', 'Data profil berhasil dihapus.');
}
public function kelembagaanAktor(string $id)
private function profileSavedResponse(BumdesRequest $request, PengurusBumdes $pengurus): RedirectResponse
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$fragment = $request->input('redirect_fragment') === 'section-kepengurusan' ? '#section-kepengurusan' : '';
$pengurus = $this->pengurusQuery()
->with([
...$this->wilayahRelationsWithoutProgramScope(),
'kelembagaanAktor',
])
->findOrFail($id);
$kelembagaanAktor = $pengurus->kelembagaanAktor;
return view('bumdes.kelembagaan-aktor.index', array_merge(
compact('pengurus', 'kelembagaanAktor'),
$this->kelembagaanAktorOptions()
));
}
public function storeKelembagaanAktor(Request $request, string $id)
{
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
abort(403);
}
$pengurus = $this->pengurusQuery()->findOrFail($id);
$validatedData = $this->validatedKelembagaanAktorData($request);
KelembagaanAktorBumdes::updateOrCreate(
['pengurus_bumdes_id' => $pengurus->id],
[
...$this->formatKelembagaanAktorData($validatedData),
'created_by' => $pengurus->kelembagaanAktor?->created_by ?? auth()->id(),
'updated_by' => auth()->id(),
]
);
return redirect()
->route('bumdes.kelembagaan-aktor.index', $pengurus->id)
->with('success', 'Data kelembagaan dan aktor BUMDes berhasil disimpan.');
}
private function pengurusQuery()
{
$query = PengurusBumdes::query();
if (auth()->user()->role->value === 'fasilitator') {
$query->where('created_by', auth()->id());
}
return $query;
}
private function provinsiOptions()
{
return Provinsi::withoutGlobalScope('program')
->select('id', 'kode', 'nama')
->orderBy('kode', 'asc')
->get();
}
private function wilayahRelationsWithoutProgramScope(): array
{
return [
'desaKelurahan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kecamatan_id'),
'desaKelurahan.kecamatan' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'kabupaten_kota_id'),
'desaKelurahan.kecamatan.kabupatenKota' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama', 'provinsi_id'),
'desaKelurahan.kecamatan.kabupatenKota.provinsi' => fn ($query) => $query->withoutGlobalScope('program')->select('id', 'nama'),
];
}
private function validatedData(Request $request): array
{
return $request->validate([
'nama' => ['required', 'string', 'max:255'],
'jenis_kelamin' => ['required', 'boolean'],
'tempat_lahir' => ['required', 'string', 'max:255'],
'tanggal_lahir' => ['required', 'date_format:d-m-Y'],
'pendidikan_terakhir' => ['required', Rule::enum(Pendidikan::class)],
'alamat' => ['required', 'string', 'max:500'],
'desa_kelurahan_id' => ['required', 'exists:master_desa_kelurahan,id'],
'nomor_telepon' => ['nullable', 'digits_between:11,13'],
'jabatan' => ['required', Rule::enum(JabatanBumdes::class)],
'jabatan_lainnya' => ['nullable', 'required_if:jabatan,' . JabatanBumdes::LAINNYA->value, 'string', 'max:255'],
'lama_menjabat_tahun' => ['required', 'integer', 'min:0', 'max:80'],
], [
'nama.required' => 'Nama harus diisi.',
'jenis_kelamin.required' => 'Jenis kelamin harus dipilih.',
'tempat_lahir.required' => 'Tempat lahir harus diisi.',
'tanggal_lahir.required' => 'Tanggal lahir harus diisi.',
'tanggal_lahir.date_format' => 'Tanggal lahir harus menggunakan format tanggal yang valid.',
'pendidikan_terakhir.required' => 'Pendidikan terakhir harus dipilih.',
'alamat.required' => 'Alamat harus diisi.',
'desa_kelurahan_id.required' => 'Desa kelurahan harus dipilih.',
'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.',
'nomor_telepon.digits_between' => 'Nomor telepon harus berupa angka 11 sampai 13 digit.',
'jabatan.required' => 'Jabatan/peran harus dipilih.',
'jabatan_lainnya.required_if' => 'Jabatan lainnya harus diisi.',
'lama_menjabat_tahun.required' => 'Lama menjabat harus diisi.',
'lama_menjabat_tahun.integer' => 'Lama menjabat harus berupa angka tahun.',
]);
}
private function formatData(array $data): array
{
$data['tanggal_lahir'] = Carbon::createFromFormat('d-m-Y', $data['tanggal_lahir'])->format('Y-m-d');
$data['tanggal_mulai_menjabat'] = Carbon::now()
->subYears((int) $data['lama_menjabat_tahun'])
->format('Y-m-d');
if (($data['jabatan'] ?? null) !== JabatanBumdes::LAINNYA->value) {
$data['jabatan_lainnya'] = null;
}
return $data;
}
private function validatedKelembagaanAktorData(Request $request): array
{
$this->normalizeKelembagaanAktorInput($request);
$maxYear = now()->year + 1;
return $request->validate([
'status_badan_hukum' => ['nullable', Rule::in(array_keys($this->statusBadanHukumOptions()))],
'nomor_dasar_hukum' => ['nullable', 'string', 'max:255'],
'tahun_dasar_hukum' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear],
'status_keaktifan' => ['nullable', Rule::in(array_keys($this->statusKeaktifanOptions()))],
'alasan_tidak_aktif' => ['nullable', 'string'],
'unit_usaha' => ['nullable', 'array'],
'unit_usaha.*' => ['string', Rule::in(array_keys($this->unitUsahaOptions()))],
'unit_usaha_jasa_lainnya' => ['nullable', 'string', 'max:255'],
'status_unit_komoditas' => ['nullable', Rule::in(array_keys($this->statusUnitKomoditasOptions()))],
'nama_unit_komoditas' => ['nullable', 'string', 'max:255'],
'status_ad_art' => ['nullable', Rule::in(array_keys($this->statusAdArtOptions()))],
'tahun_ad_art' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear],
'pengurus_inti' => ['nullable', 'array'],
'pengurus_inti.*.jabatan' => ['nullable', 'string', 'max:255'],
'pengurus_inti.*.nama' => ['nullable', 'string', 'max:255'],
'pengurus_inti.*.aktif' => ['nullable', 'boolean'],
'memiliki_staf' => ['nullable', 'boolean'],
'jumlah_staf' => ['nullable', 'integer', 'min:0'],
'unit_usaha_staf' => ['nullable', 'string'],
'modal_awal' => ['nullable', 'integer', 'min:0'],
'tahun_modal_awal' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear],
'pernah_penambahan_modal' => ['nullable', 'boolean'],
'penambahan_modal' => ['nullable', 'array'],
'penambahan_modal.*.tahun' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:' . $maxYear],
'penambahan_modal.*.jumlah' => ['nullable', 'integer', 'min:0'],
'aset_produktif' => ['nullable', 'array'],
'aset_produktif.*.aset' => ['nullable', 'string', 'max:255'],
'aset_produktif.*.dipilih' => ['nullable', 'boolean'],
'aset_produktif.*.jumlah' => ['nullable', 'string', 'max:255'],
'aset_produktif.*.kondisi' => ['nullable', 'string', 'max:255'],
'aset_produktif_lainnya' => ['nullable', 'string', 'max:255'],
'status_kepemilikan_aset' => ['nullable', Rule::in(array_keys($this->statusKepemilikanAsetOptions()))],
'infrastruktur_pascapanen' => ['nullable', 'array'],
'infrastruktur_pascapanen.*.infrastruktur' => ['nullable', 'string', 'max:255'],
'infrastruktur_pascapanen.*.dipilih' => ['nullable', 'boolean'],
'infrastruktur_pascapanen.*.jumlah' => ['nullable', 'string', 'max:255'],
'infrastruktur_pascapanen.*.kondisi' => ['nullable', Rule::in(array_keys($this->kondisiInfrastrukturOptions()))],
'infrastruktur_pascapanen_lainnya' => ['nullable', 'string', 'max:255'],
], $this->kelembagaanAktorValidationMessages(), $this->kelembagaanAktorValidationAttributes());
}
private function normalizeKelembagaanAktorInput(Request $request): void
{
foreach (['modal_awal'] as $field) {
if ($request->has($field)) {
$request->merge([$field => preg_replace('/\D/', '', (string) $request->input($field))]);
}
}
$penambahanModal = collect($request->input('penambahan_modal', []))
->map(function ($row) {
if (isset($row['jumlah'])) {
$row['jumlah'] = preg_replace('/\D/', '', (string) $row['jumlah']);
}
return $row;
})
->all();
if ($request->has('penambahan_modal')) {
$request->merge(['penambahan_modal' => $penambahanModal]);
}
}
private function formatKelembagaanAktorData(array $data): array
{
$payload = array_merge([
'status_badan_hukum' => null,
'nomor_dasar_hukum' => null,
'tahun_dasar_hukum' => null,
'status_keaktifan' => null,
'alasan_tidak_aktif' => null,
'unit_usaha' => [],
'unit_usaha_jasa_lainnya' => null,
'status_unit_komoditas' => null,
'nama_unit_komoditas' => null,
'status_ad_art' => null,
'tahun_ad_art' => null,
'pengurus_inti' => [],
'memiliki_staf' => null,
'jumlah_staf' => null,
'unit_usaha_staf' => null,
'modal_awal' => null,
'tahun_modal_awal' => null,
'pernah_penambahan_modal' => null,
'penambahan_modal' => [],
'aset_produktif' => [],
'aset_produktif_lainnya' => null,
'status_kepemilikan_aset' => null,
'infrastruktur_pascapanen' => [],
'infrastruktur_pascapanen_lainnya' => null,
], $data);
$payload['unit_usaha'] = array_values($payload['unit_usaha'] ?? []);
$payload['pengurus_inti'] = $this->filterRows($payload['pengurus_inti'] ?? [], ['nama', 'aktif']);
$payload['penambahan_modal'] = $this->filterRows($payload['penambahan_modal'] ?? [], ['tahun', 'jumlah']);
$payload['aset_produktif'] = $this->filterRows($payload['aset_produktif'] ?? [], ['dipilih', 'jumlah', 'kondisi']);
$payload['infrastruktur_pascapanen'] = $this->filterRows($payload['infrastruktur_pascapanen'] ?? [], ['dipilih', 'jumlah', 'kondisi']);
if (($payload['status_keaktifan'] ?? null) !== 'tidak_aktif_vakum') {
$payload['alasan_tidak_aktif'] = null;
}
if (! in_array('jasa_lainnya', $payload['unit_usaha'], true)) {
$payload['unit_usaha_jasa_lainnya'] = null;
}
if (($payload['status_unit_komoditas'] ?? null) !== 'ya') {
$payload['nama_unit_komoditas'] = null;
}
if (($payload['status_ad_art'] ?? null) !== 'ada_lengkap') {
$payload['tahun_ad_art'] = null;
}
if (! (bool) ($payload['memiliki_staf'] ?? false)) {
$payload['jumlah_staf'] = null;
$payload['unit_usaha_staf'] = null;
}
if (! (bool) ($payload['pernah_penambahan_modal'] ?? false)) {
$payload['penambahan_modal'] = null;
}
if (! collect($payload['aset_produktif'] ?? [])->contains(fn ($row) => ($row['aset'] ?? null) === 'lainnya')) {
$payload['aset_produktif_lainnya'] = null;
}
if (! collect($payload['infrastruktur_pascapanen'] ?? [])->contains(fn ($row) => ($row['infrastruktur'] ?? null) === 'lainnya')) {
$payload['infrastruktur_pascapanen_lainnya'] = null;
}
return $payload;
}
private function filterRows(array $rows, array $valueKeys): array
{
return collect($rows)
->map(function ($row) {
return collect($row)
->map(fn ($value) => $value === '' ? null : $value)
->all();
})
->filter(function ($row) use ($valueKeys) {
foreach ($valueKeys as $key) {
if (filled($row[$key] ?? null)) {
return true;
}
}
return false;
})
->values()
->all();
}
private function kelembagaanAktorOptions(): array
{
return [
'statusBadanHukumOptions' => $this->statusBadanHukumOptions(),
'statusKeaktifanOptions' => $this->statusKeaktifanOptions(),
'unitUsahaOptions' => $this->unitUsahaOptions(),
'statusUnitKomoditasOptions' => $this->statusUnitKomoditasOptions(),
'statusAdArtOptions' => $this->statusAdArtOptions(),
'pengurusIntiRows' => $this->pengurusIntiRows(),
'asetProduktifRows' => $this->asetProduktifRows(),
'statusKepemilikanAsetOptions' => $this->statusKepemilikanAsetOptions(),
'infrastrukturPascapanenRows' => $this->infrastrukturPascapanenRows(),
'kondisiInfrastrukturOptions' => $this->kondisiInfrastrukturOptions(),
];
}
private function statusBadanHukumOptions(): array
{
return [
'sudah_berbadan_hukum' => 'Sudah berbadan hukum (terdaftar Kemenkumham)',
'proses_pendaftaran' => 'Proses pendaftaran',
'belum_berbadan_hukum' => 'Belum berbadan hukum',
'tidak_tahu' => 'Tidak tahu',
];
}
private function statusKeaktifanOptions(): array
{
return [
'aktif_penuh' => 'Aktif beroperasi penuh',
'aktif_terbatas' => 'Aktif tapi terbatas (sebagian unit usaha berjalan)',
'tidak_aktif_vakum' => 'Tidak aktif/vakum',
'berdiri_belum_beroperasi' => 'Sudah berdiri tapi belum pernah beroperasi',
];
}
private function unitUsahaOptions(): array
{
return [
'simpan_pinjam' => 'Simpan pinjam',
'perdagangan_toko_desa' => 'Perdagangan/toko desa',
'pengelolaan_air_bersih' => 'Pengelolaan air bersih',
'wisata_desa_agrowisata' => 'Wisata desa/agrowisata',
'pengelolaan_hasil_pertanian_komoditas' => 'Pengelolaan hasil pertanian/komoditas',
'penyewaan_alat_aset' => 'Penyewaan alat/aset',
'jasa_lainnya' => 'Jasa lainnya',
];
}
private function statusUnitKomoditasOptions(): array
{
return [
'ya' => 'Ya, ada unit khusus',
'ditangani_pengurus_inti' => 'Tidak ada unit khusus, ditangani langsung pengurus inti',
'direncanakan' => 'Belum ada, masih direncanakan',
];
}
private function statusAdArtOptions(): array
{
return [
'ada_lengkap' => 'Ada, lengkap',
'ada_tidak_lengkap' => 'Ada tapi tidak lengkap',
'tidak_ada' => 'Tidak ada',
'tidak_tahu' => 'Tidak tahu',
];
}
private function pengurusIntiRows(): array
{
return [
'penasihat_kepala_desa' => 'Penasihat (Kepala Desa)',
'ketua_pengawas' => 'Ketua Pengawas',
'direktur_ketua_bumdes' => 'Direktur/Ketua BUMDes',
'sekretaris' => 'Sekretaris',
'bendahara' => 'Bendahara',
'kepala_unit_usaha_komoditas' => 'Kepala Unit Usaha Komoditas',
'lainnya' => 'Lainnya',
];
}
private function asetProduktifRows(): array
{
return [
'bangunan_kantor' => 'Bangunan / kantor',
'gudang_penyimpanan' => 'Gudang penyimpanan',
'kendaraan_operasional_angkut' => 'Kendaraan operasional/angkut',
'lahan_usaha' => 'Lahan usaha',
'alat_pengolahan_komoditas' => 'Alat pengolahan komoditas',
'lainnya' => 'Lainnya',
];
}
private function statusKepemilikanAsetOptions(): array
{
return [
'milik_bumdes_sendiri' => 'Milik BUMDes sendiri',
'milik_desa_dipinjamkan_dikelola' => 'Milik desa (dipinjamkan/dikelola)',
'sewa_pinjam_pihak_lain' => 'Sewa/pinjam pihak lain',
];
}
private function infrastrukturPascapanenRows(): array
{
return [
'gudang_penyimpanan_hasil_panen' => 'Gudang penyimpanan hasil panen',
'alat_mesin_pengering' => 'Alat/mesin pengering',
'mesin_pulper_huller' => 'Mesin pulper/huller',
'timbangan' => 'Timbangan',
'alat_sortasi' => 'Alat sortasi',
'lainnya' => 'Lainnya',
];
}
private function kondisiInfrastrukturOptions(): array
{
return [
'baik_berfungsi' => 'Baik dan berfungsi',
'rusak_tidak_terpakai' => 'Rusak/tidak terpakai',
];
}
private function kelembagaanAktorValidationMessages(): array
{
return [
'integer' => ':attribute harus berupa angka.',
'digits' => ':attribute harus terdiri dari :digits digit.',
'min' => ':attribute tidak boleh kurang dari :min.',
'max' => ':attribute tidak valid.',
'in' => ':attribute tidak valid.',
'array' => ':attribute harus berupa daftar data.',
'boolean' => ':attribute harus dipilih.',
];
}
private function kelembagaanAktorValidationAttributes(): array
{
return [
'status_badan_hukum' => 'Status badan hukum',
'nomor_dasar_hukum' => 'Nomor dasar hukum',
'tahun_dasar_hukum' => 'Tahun dasar hukum',
'status_keaktifan' => 'Status keaktifan',
'unit_usaha' => 'Unit usaha',
'status_unit_komoditas' => 'Unit usaha komoditas',
'status_ad_art' => 'Dokumen AD/ART',
'memiliki_staf' => 'Status staf/karyawan',
'jumlah_staf' => 'Jumlah staf',
'modal_awal' => 'Modal awal',
'tahun_modal_awal' => 'Tahun modal awal',
'pernah_penambahan_modal' => 'Status penambahan modal',
'status_kepemilikan_aset' => 'Status kepemilikan aset',
];
return redirect(route('bumdes.edit', $pengurus).$fragment)->with('success', 'Data Profil berhasil disimpan.');
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Web\Bumdes;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bumdes\BumdesRequest;
use App\Http\Requests\Bumdes\StoreRantaiNilaiBumdesRequest;
use App\Models\Bumdes\PengurusBumdes;
use App\Services\Bumdes\PengurusBumdesService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class RantaiNilaiBumdesController extends Controller
{
public function __construct(private readonly PengurusBumdesService $service) {}
public function index(BumdesRequest $request, PengurusBumdes $pengurusBumdes): View
{
return view('bumdes.rantai-nilai.index', $this->service->sectionData($pengurusBumdes));
}
public function store(StoreRantaiNilaiBumdesRequest $request, PengurusBumdes $pengurusBumdes): RedirectResponse
{
$this->service->saveSection($pengurusBumdes, $request->validated(), 'rantai-nilai');
$nextSection = $request->validated('redirect_fragment');
$postharvestSection = in_array($nextSection, ['section-rantai-pengeringan', 'section-rantai-fermentasi', 'section-rantai-premium-harga'], true);
$canContinue = $request->boolean($postharvestSection ? 'melakukan_pengolahan_pascapanen' : 'melakukan_jual_beli');
$nextSection = $canContinue ? $nextSection : null;
return redirect()->to(route('bumdes.rantai-nilai.index', $pengurusBumdes).($nextSection ? '#'.$nextSection : ''))
->with('success', 'Data Rantai Nilai & Akses Pasar berhasil disimpan.');
}
}
@@ -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;
}
}
@@ -17,7 +17,7 @@ class DemografiPetaniController extends Controller
{
public function index(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -28,7 +28,7 @@ class DemografiPetaniController extends Controller
public function karakteristik(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -94,7 +94,7 @@ class IndeksPetaniController extends Controller
public function index(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -0,0 +1,210 @@
<?php
namespace App\Http\Controllers\Web\Dashboard;
use App\Http\Controllers\Controller;
use App\Services\AlphaVantageMarket;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Throwable;
class InternationalMarketController extends Controller
{
public function __invoke()
{
abort_unless(Gate::allows('super_admin') || Gate::allows('admin') || Gate::allows('koordinator') || Gate::allows('enumerator'), 403);
// File cache keeps external quotes independent of the application database.
$cache = Cache::store('file');
$marketData = $cache->remember('market.yahoo.quotes.v3', 300, function () use ($cache) {
$commodities = [
['name' => 'Kopi Arabica', 'symbol' => 'KC=F'],
['name' => 'Kakao (Cocoa)', 'symbol' => 'CC=F'],
];
try {
$responses = Http::pool(function (Pool $pool) use ($commodities) {
return array_map(fn ($commodity) => $pool->as($commodity['symbol'])
->acceptJson()->withHeaders(['User-Agent' => 'Mozilla/5.0'])
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
->connectTimeout(3)->timeout(8)
->get('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($commodity['symbol']), [
'interval' => '1d', 'range' => '5d',
]), [...$commodities, ['symbol' => 'IDR=X']]);
});
} catch (Throwable $exception) {
report($exception);
$responses = [];
}
$exchangeRate = $cache->get('market.yahoo.usd_idr');
if (is_array($exchangeRate)) {
$exchangeRate['stale'] = true;
}
$rateResponse = $responses['IDR=X'] ?? null;
if ($rateResponse instanceof \Illuminate\Http\Client\Response && $rateResponse->successful()) {
$meta = $rateResponse->json('chart.result.0.meta');
if (is_array($meta) && ($meta['symbol'] ?? null) === 'IDR=X'
&& ($meta['currency'] ?? null) === 'IDR'
&& is_numeric($meta['regularMarketPrice'] ?? null) && $meta['regularMarketPrice'] > 0
&& is_numeric($meta['regularMarketTime'] ?? null)) {
$exchangeRate = [
'value' => (float) $meta['regularMarketPrice'],
'quoted_at' => gmdate('c', (int) $meta['regularMarketTime']),
'source' => 'Yahoo Finance',
'source_url' => 'https://finance.yahoo.com/quote/IDR%3DX/',
'stale' => false,
];
$cache->put('market.yahoo.usd_idr', $exchangeRate, now()->addDays(7));
}
}
$quotes = array_map(function ($commodity) use ($responses, $cache) {
$symbol = $commodity['symbol'];
$key = 'market.yahoo.last.'.rawurlencode($symbol);
$quote = $commodity + [
'source' => 'Yahoo Finance',
'source_url' => 'https://finance.yahoo.com/quote/'.rawurlencode($symbol).'/',
'price' => null, 'currency' => null, 'unit' => null,
'quoted_at' => null, 'stale' => false,
];
try {
$response = $responses[$symbol] ?? null;
if (!$response instanceof \Illuminate\Http\Client\Response || !$response->successful()) {
throw new \RuntimeException('Quote unavailable');
}
$meta = $response->json('chart.result.0.meta');
if ($response->json('chart.error') || !is_array($meta)
|| ($meta['symbol'] ?? null) !== $symbol
|| !is_numeric($meta['regularMarketPrice'] ?? null)
|| (float) $meta['regularMarketPrice'] <= 0
|| !is_numeric($meta['regularMarketTime'] ?? null)
|| !is_string($meta['currency'] ?? null)) {
throw new \RuntimeException('Invalid quote');
}
$quote['price'] = (float) $meta['regularMarketPrice'];
$quote['currency'] = $meta['currency'];
// Yahoo quotes Arabica in US cents/lb; cocoa and robusta in USD/tonne.
// Apply the unit only when the API currency matches that convention.
$quote['unit'] = match (true) {
$symbol === 'KC=F' && $meta['currency'] === 'USX' => 'sen AS / lb',
$symbol === 'CC=F' && $meta['currency'] === 'USD' => 'USD / ton metrik',
default => $meta['currency'].' (satuan mengikuti sumber)',
};
$quote['quoted_at'] = gmdate('c', (int) $meta['regularMarketTime']);
$cache->put($key, $quote, now()->addDays(7));
return $quote;
} catch (Throwable $exception) {
$previous = $cache->get($key);
if (is_array($previous)) {
$previous['stale'] = true;
return $previous;
}
return $quote;
}
}, $commodities);
return ['quotes' => $quotes, 'exchange_rate' => $exchangeRate];
});
$quotes = $marketData['quotes'];
$exchangeRate = $marketData['exchange_rate'];
foreach ($quotes as &$quote) {
$quote['id'] = 'yahoo:'.$quote['symbol'];
$quote['description'] = 'Estimasi konversi harga kontrak berjangka internasional.';
$quote['period'] = $quote['quoted_at'];
$quote['frequency'] = 'market';
}
unset($quote);
$quotes = array_merge($quotes, $this->imfQuotes($cache), app(AlphaVantageMarket::class)->quotes($cache));
$quotes = array_values(array_filter($quotes, fn ($quote) => is_numeric($quote['price']) && is_finite((float) $quote['price']) && $quote['price'] > 0));
foreach ($quotes as &$quote) {
// All active providers quote either US cents/lb or USD/metric tonne.
$usdPerKg = match ($quote['unit']) {
'sen AS / lb' => $quote['price'] / 100 / 0.45359237,
'USD / ton metrik' => $quote['price'] / 1000,
default => null,
};
$quote['price_idr_per_kg'] = $exchangeRate && $usdPerKg !== null
? round($usdPerKg * $exchangeRate['value'], 2) : null;
unset($quote['usd_per_kg']); // Old Alpha cache entries may contain this field.
}
unset($quote);
return response()->json([
'quotes' => $quotes,
'exchange_rate' => $exchangeRate,
])->header('Cache-Control', 'no-store');
}
private function imfQuotes(Repository $cache): array
{
// IMF observations are monthly averages; fetch at most once a day.
return $cache->remember('market.imf.monthly.v1', 86400, function () use ($cache) {
try {
$response = Http::withHeaders(['Accept' => 'text/csv', 'User-Agent' => 'Mozilla/5.0'])
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
->connectTimeout(3)->timeout(10)
->get('https://api.imf.org/external/sdmx/2.1/data/IMF.RES,PCPS/G001.PCOFFOTM+PCOFFROB+PCOCO.USD.M', [
'startPeriod' => now()->subYears(2)->format('Y-m'), 'format' => 'csvfile',
])->throw();
$stream = fopen('php://memory', 'r+');
try {
fwrite($stream, $response->body());
rewind($stream);
$headers = fgetcsv($stream, 0, ',', '"', '');
if (!$headers || array_diff(['COUNTRY', 'INDICATOR', 'DATA_TRANSFORMATION', 'FREQUENCY', 'TIME_PERIOD', 'OBS_VALUE', 'SCALE'], $headers)) {
throw new \RuntimeException('Unexpected IMF CSV format');
}
$latest = [];
while (($values = fgetcsv($stream, 0, ',', '"', '')) !== false) {
if (count($values) !== count($headers)) continue;
$row = array_combine($headers, $values);
if ($row['COUNTRY'] !== 'G001' || $row['DATA_TRANSFORMATION'] !== 'USD' || $row['FREQUENCY'] !== 'M'
|| $row['SCALE'] !== '0' || !is_numeric($row['OBS_VALUE']) || $row['OBS_VALUE'] <= 0
|| !preg_match('/^\d{4}-M(0[1-9]|1[0-2])$/', $row['TIME_PERIOD'])) continue;
$indicator = $row['INDICATOR'];
if (!isset($latest[$indicator]) || $row['TIME_PERIOD'] > $latest[$indicator]['TIME_PERIOD']) {
$latest[$indicator] = $row;
}
}
} finally {
fclose($stream);
}
$definitions = [
'PCOFFOTM' => ['Kopi Arabica', 'USX', 'sen AS / lb', 'Biji kopi Arabica (Other Milds). Estimasi konversi harga acuan rata-rata bulanan IMF.'],
'PCOFFROB' => ['Kopi Robusta', 'USX', 'sen AS / lb', 'Biji kopi Robusta. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
'PCOCO' => ['Kakao (Cocoa)', 'USD', 'USD / ton metrik', 'Biji kakao. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
];
$quotes = [];
foreach ($definitions as $indicator => [$name, $currency, $unit, $description]) {
if (!isset($latest[$indicator])) continue;
$row = $latest[$indicator];
$quotes[] = [
'id' => 'imf:'.$indicator, 'symbol' => $indicator, 'name' => $name,
'price' => (float) $row['OBS_VALUE'], 'currency' => $currency, 'unit' => $unit,
'source' => 'IMF · PCPS', 'source_url' => 'https://data.imf.org/Datasets/PCPS',
'description' => $description, 'frequency' => 'monthly',
'period' => str_replace('-M', '-', $row['TIME_PERIOD']), 'stale' => false,
];
}
if (!$quotes) throw new \RuntimeException('No valid IMF observations');
$cache->put('market.imf.last.v1', $quotes, now()->addDays(30));
return $quotes;
} catch (Throwable $exception) {
report($exception);
return array_map(function ($quote) {
$quote['stale'] = true;
return $quote;
}, $cache->get('market.imf.last.v1', []));
}
});
}
}
@@ -17,7 +17,7 @@ class ProfilPetaniController extends Controller
{
public function index()
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
$title = 'Dashboard Profil Petani';
@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Web\Dashboard;
use App\Enums\AksesPasar\PraktikFermentasi;
use App\Enums\AksesPasar\SkorKesadaranPremium;
use App\Enums\AksesPasar\SkorPengalaman;
use App\Enums\KeparahanSerangan;
use App\Enums\KondisiTanaman;
use App\Enums\PenyakitTanaman;
@@ -35,7 +37,7 @@ class SummaryController extends Controller
public function produktivitas(Request $request)
{
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('koordinator') && !Gate::allows('enumerator')) {
abort(403);
}
@@ -57,7 +59,7 @@ class SummaryController extends Controller
public function mapInformation(Request $request)
{
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('koordinator') && !Gate::allows('enumerator')) {
abort(403);
}
@@ -99,7 +101,7 @@ class SummaryController extends Controller
public function pasar(Request $request)
{
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('fasilitator')) {
if (!Gate::allows('super_admin') && !Gate::allows('admin') && !Gate::allows('koordinator') && !Gate::allows('enumerator')) {
abort(403);
}
@@ -114,70 +116,196 @@ class SummaryController extends Controller
'kecamatan_id',
'desaKelurahan_id',
'komoditas_id',
'segmen',
]),
] + $filterData + $this->marketDashboardData($request));
}
private function marketDashboardData(Request $request): array
{
if (! $request->filled('komoditas_id')) {
return [
'selectedKomoditas' => null,
'priceCard' => null,
];
}
$selectedKomoditas = Komoditas::query()
->whereKey($request->komoditas_id)
->first(['id', 'nama']);
if (! $selectedKomoditas) {
return [
'selectedKomoditas' => null,
'priceCard' => null,
];
}
$productCaption = $this->marketProductCaption($selectedKomoditas->nama);
$productLabel = 'Buah segar hasil petik';
$products = [
['name' => 'Kakao', 'caption' => 'Harga Biji Kering', 'product' => 'Biji kering/Biji Kupas', 'variant' => 'success'],
['name' => 'Vanili', 'caption' => 'Harga Buah Segar', 'product' => 'Buah segar hasil petik', 'variant' => 'primary'],
['name' => 'Kopi', 'caption' => 'Harga Ceri Merah', 'product' => 'Buah segar hasil petik', 'variant' => 'warning'],
['name' => 'Kemiri', 'caption' => 'Harga Gelondong', 'product' => 'Buah segar hasil petik', 'variant' => 'info'],
];
$commodities = Komoditas::query()
->whereIn('nama', array_column($products, 'name'))
->pluck('id', 'nama');
$petaniIds = $this->filteredPetaniIdsQuery($request);
$priceQuery = DB::table('harga_jual_panen')
$prices = DB::table('harga_jual_panen')
->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id')
->whereIn('harga_jual_panen.petani_id', $petaniIds)
->whereIn('petani.komoditas_id', $commodities->values())
->where('harga_jual_panen.harga_panen_terakhir', '>', 0)
->where('harga_jual_panen.bentuk_produk', $productLabel);
$averagePrice = (float) (clone $priceQuery)->avg('harga_jual_panen.harga_panen_terakhir');
$totalRecords = (int) (clone $priceQuery)->count();
->whereIn('harga_jual_panen.bentuk_produk', array_unique(array_column($products, 'product')))
->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
->selectRaw('AVG(harga_jual_panen.harga_panen_terakhir) as average_price, COUNT(*) as records')
->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
->get();
return [
'selectedKomoditas' => $selectedKomoditas,
'priceCard' => [
'value' => $averagePrice > 0
? 'Rp '.number_format($averagePrice, 0, ',', '.')
: 'Rp 0',
'label' => 'Harga Jual',
'caption' => $productCaption,
'description' => 'Harga rata-rata buah segar hasil petik di tingkat petani',
'records' => $totalRecords,
'unit' => '/kg',
'variant' => 'success',
'icon' => 'tabler-cash',
],
'priceCards' => collect($products)->map(function (array $product) use ($commodities, $prices) {
$commodityId = $commodities->get($product['name']);
$price = $commodityId === null ? null : $prices->first(fn ($row) =>
(int) $row->komoditas_id === (int) $commodityId
&& $row->bentuk_produk === $product['product']
);
return [
'value' => 'Rp '.number_format((float) ($price?->average_price ?? 0), 0, ',', '.'),
'label' => $product['name'].' - '.$product['caption'],
'description' => 'Harga rata-rata panen terakhir di tingkat petani',
'records' => (int) ($price?->records ?? 0),
'unit' => '/kg',
'variant' => $product['variant'],
'icon' => 'tabler-cash',
];
})->all(),
'marketCharts' => $this->marketChannelCharts($request),
'priceTrend' => $this->marketPriceTrend($request),
'premiumAwareness' => $this->marketPremiumAwareness($request),
];
}
private function marketProductCaption(string $komoditasName): string
private function marketPriceTrend(Request $request): array
{
$normalized = str($komoditasName)->lower()->trim()->toString();
$products = ['Buah segar hasil petik', 'Biji basah', 'Biji kering/Biji Kupas', 'Terfermentasi (Rp/kg)'];
$periods = [
'harga_dua_tahun_lalu' => 'Dua Tahun Lalu',
'harga_tahun_lalu' => 'Tahun Lalu',
'harga_panen_terakhir' => 'Panen Terakhir',
];
$commodities = Komoditas::query()
->when($request->filled('komoditas_id'), fn ($query) => $query->where('id', $request->komoditas_id))
->orderBy('nama')
->get(['id', 'nama']);
return match (true) {
str_contains($normalized, 'kakao') => 'Harga Buah Kakao',
str_contains($normalized, 'vanili') => 'Harga Curing',
str_contains($normalized, 'kopi') => 'Harga Ceri Merah',
str_contains($normalized, 'kemiri') => 'Harga Gelondong',
default => 'Harga Buah Segar',
};
$query = DB::table('harga_jual_panen')
->join('petani', 'petani.id', '=', 'harga_jual_panen.petani_id')
->whereIn('harga_jual_panen.petani_id', $this->filteredPetaniIdsQuery($request))
->whereIn('petani.komoditas_id', $commodities->pluck('id'))
->whereIn('harga_jual_panen.bentuk_produk', $products)
->select('petani.komoditas_id', 'harga_jual_panen.bentuk_produk')
->groupBy('petani.komoditas_id', 'harga_jual_panen.bentuk_produk');
foreach (array_keys($periods) as $field) {
$query->selectRaw("AVG(CASE WHEN harga_jual_panen.{$field} > 0 THEN harga_jual_panen.{$field} END) as {$field}");
}
$prices = $query->get()->groupBy('komoditas_id');
return [
'periods' => array_values($periods),
'commodities' => $commodities->map(function ($commodity) use ($products, $periods, $prices) {
$rows = $prices->get($commodity->id, collect())->keyBy('bentuk_produk');
return [
'id' => $commodity->id,
'name' => $commodity->nama,
'series' => collect($products)->map(function ($product) use ($periods, $rows) {
$row = $rows->get($product);
return [
'name' => $product === 'Terfermentasi (Rp/kg)' ? 'Terfermentasi' : $product,
'data' => array_map(fn ($field) => isset($row->{$field}) ? round((float) $row->{$field}, 2) : null, array_keys($periods)),
];
})->all(),
];
})->all(),
];
}
private function marketChannelCharts(Request $request): array
{
$rows = DB::table('akses_pasar')
->whereNull('deleted_at')
->whereIn('petani_id', $this->filteredPetaniIdsQuery($request))
->orderBy('id')
->get(['petani_id', 'tujuan_penjualan', 'sifat_hubungan_bisnis', 'frekuensi_penjualan', 'skor_pengalaman'])
->unique('petani_id');
$channels = ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Langsung ke pembeli/eksportir', 'Lainnya'];
$relationships = [
'Bebas, bisa pindah pembeli kapan saja' => 'Bebas pindah pembeli',
'Langganan tetap, tapi tidak terikat' => 'Langganan tetap',
'Berbasis hutang, harus jual ke sana karena ada pinjaman' => 'Berbasis hutang',
'Titip jual,dibayar setelah produk terjual' => 'Titip jual',
'Ada komitmen volume, sudah ada kesepakatan jumlah' => 'Komitmen volume',
'Semi-kontrak, ada kesepakatan harga tertentu' => 'Semi-kontrak',
];
$timing = ['Langsung setelah panen', 'Ditumpuk dulu beberapa waktu', 'Saat butuh uang mendesak'];
$experience = collect(SkorPengalaman::options())
->mapWithKeys(fn ($option) => [$option['value'] => $option['label']])
->all();
return [
'saluranPenjualan' => $this->marketResponseChart($rows, 'tujuan_penjualan', array_combine($channels, $channels), true),
'hubunganPembeli' => $this->marketResponseChart($rows, 'sifat_hubungan_bisnis', $relationships),
'waktuPenjualan' => $this->marketResponseChart($rows, 'frekuensi_penjualan', array_combine($timing, $timing)),
'aksesSaluranLain' => $this->marketResponseChart($rows, 'skor_pengalaman', $experience),
];
}
private function marketPremiumAwareness(Request $request): array
{
$rows = DB::table('akses_pasar')
->whereNull('deleted_at')
->whereIn('petani_id', $this->filteredPetaniIdsQuery($request))
->orderBy('id')
->get(['petani_id', 'skor_kesadaran_premium'])
->unique('petani_id');
$options = collect(SkorKesadaranPremium::options())
->mapWithKeys(fn ($option) => [$option['value'] => $option['label']])
->all();
$chart = $this->marketResponseChart($rows, 'skor_kesadaran_premium', $options);
$chart['fullLabels'] = [
'Belum pernah mendengar sama sekali',
'Pernah mendengar tapi belum paham persyaratannya',
'Pernah mendengar dan sedikit memahami persyaratannya',
'Sudah sangat memahami persyaratannya, tapi belum pernah mengakses pasar premium',
'Sudah mengakses dan mendapat harga premium secara konsisten',
];
return $chart;
}
private function marketResponseChart($rows, string $field, array $options, bool $multiple = false): array
{
$counts = array_fill_keys(array_keys($options), 0);
$respondents = 0;
foreach ($rows as $row) {
$values = $multiple ? $this->arrayValues($row->{$field}) : [$row->{$field}];
$answers = [];
foreach ($values as $value) {
if (is_string($value) || is_int($value)) {
$value = is_string($value) ? trim($value) : $value;
if (array_key_exists($value, $options)) {
$answers[$value] = true;
}
}
}
if ($answers === []) {
continue;
}
$respondents++;
foreach (array_keys($answers) as $answer) {
$counts[$answer]++;
}
}
return [
'labels' => array_values($options),
'fullLabels' => $field === 'skor_pengalaman' ? array_values($options) : array_keys($options),
'series' => array_values($counts),
'respondents' => $respondents,
'percentages' => array_map(fn ($count) => $respondents > 0 ? round($count / $respondents * 100, 1) : 0, array_values($counts)),
];
}
private function productionDashboardData(Request $request): array
@@ -223,7 +351,6 @@ class SummaryController extends Controller
$healthSourceName = $tanaman->isNotEmpty() ? 'tanaman' : 'lahan';
$landMetrics = $this->karakteristikLandMetrics($request);
$totalPetani = $landMetrics['total_petani'];
$totalLuasLahan = $landMetrics['total_luas_lahan'];
$avgLuasPerPetani = $landMetrics['ratarata_luas_lahan'];
$actualProductivity = $this->actualProductivityByCommodityChart($request);
@@ -233,14 +360,6 @@ class SummaryController extends Controller
$aksesPasar = $this->filteredAksesPasar($request);
$kpiCards = [
[
'value' => number_format($totalPetani, 0, ',', '.'),
'unit' => 'orang',
'label' => 'Jumlah Petani',
'description' => 'Petani sesuai filter yang dipilih',
'icon' => 'tabler-users',
'variant' => 'primary',
],
[
'value' => number_format($totalLuasLahan, 2, ',', '.'),
'unit' => 'ha',
@@ -294,7 +413,7 @@ class SummaryController extends Controller
: $value,
),
'praktikSortasiPanen' => $this->enumOptionChart($surveyPanen, 'praktik_sortasi', PraktikSortasi::class),
'praktikFermentasiPascapanen' => $this->fermentationPracticeChart($aksesPasar),
'praktikFermentasiPascapanen' => $this->fermentationPracticeByCommodityChart($aksesPasar),
'produktivitasAktualPerKomoditas' => $actualProductivity,
],
];
@@ -312,7 +431,6 @@ class SummaryController extends Controller
'desa' => (string) $request->input('desaKelurahan_id', ''),
],
'levels' => $levels,
'cultivationLevels' => $this->productionCultivationMapLevelData($request, $levels),
'information' => $this->productionMapInformationData($request),
];
}
@@ -511,6 +629,7 @@ class SummaryController extends Controller
'map_lahan.path',
'map_lahan.luas_lahan',
'map_lahan.jumlah_tanaman',
'map_lahan.petani_id',
'map_lahan.umur_tanaman',
'map_lahan.serangan_hama',
'map_lahan.penyakit_tanaman',
@@ -537,58 +656,10 @@ class SummaryController extends Controller
])
->groupBy('lahan_id');
$geoLahan = $mapLahan
->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
->map(function ($lahan) use ($tanamanByLahan) {
$tanamanRows = $tanamanByLahan->get($lahan->id, collect());
$komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
$opt = $this->productionOptDetails($lahan, $tanamanRows);
return [
'id' => $lahan->id,
'parent_id' => $lahan->desa_kelurahan_id,
'polygon' => $lahan->path,
'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
'luas' => (float) $lahan->luas_lahan,
'komoditas' => $komoditas,
'opt' => $opt,
];
})
->values()
->all();
return [
'provinsi' => array_values($geoProvinsi),
'kabupaten' => array_values($geoKabupaten),
'kecamatan' => array_values($geoKecamatan),
'desa' => array_values($geoDesa),
'lahan' => $geoLahan,
];
}
private function productionCultivationMapLevelData(Request $request, array $baseLevels): array
{
$mapLahan = (clone $this->filteredLahanQuery($request))
->whereNotNull('map_lahan.path')
->leftJoin('profiles', 'profiles.id', '=', 'map_lahan.profile_id')
->leftJoin('petani', 'petani.id', '=', 'map_lahan.petani_id')
->tap(fn ($query) => $this->whereActiveMapOwner($query))
->orderByDesc('map_lahan.updated_at')
->limit(500)
->get([
'map_lahan.id',
'map_lahan.desa_kelurahan_id',
'map_lahan.path',
'map_lahan.luas_lahan',
'map_lahan.petani_id',
'profiles.nama as profile_nama',
'petani.nama as petani_nama',
]);
$petaniIds = $mapLahan->pluck('petani_id')->filter()->unique()->values();
$practiceByPetani = DB::table('praktik_budidaya')
->whereNull('praktik_budidaya.deleted_at')
->when($petaniIds->isNotEmpty(), fn ($query) => $query->whereIn('praktik_budidaya.petani_id', $petaniIds))
->when($petaniIds->isEmpty(), fn ($query) => $query->whereRaw('1 = 0'))
->whereIn('praktik_budidaya.petani_id', $petaniIds)
->orderByDesc('praktik_budidaya.updated_at')
->get([
'praktik_budidaya.petani_id',
@@ -601,7 +672,10 @@ class SummaryController extends Controller
$geoLahan = $mapLahan
->filter(fn ($lahan) => $this->hasMapPolygon($lahan->path))
->map(function ($lahan) use ($practiceByPetani) {
->map(function ($lahan) use ($tanamanByLahan, $practiceByPetani) {
$tanamanRows = $tanamanByLahan->get($lahan->id, collect());
$komoditas = $this->productionCommodityDetails($lahan, $tanamanRows);
$opt = $this->productionOptDetails($lahan, $tanamanRows);
$practice = $practiceByPetani->get($lahan->petani_id);
return [
@@ -610,6 +684,8 @@ class SummaryController extends Controller
'polygon' => $lahan->path,
'nama_petani' => $lahan->petani_nama ?? $lahan->profile_nama ?? 'N/A',
'luas' => (float) $lahan->luas_lahan,
'komoditas' => $komoditas,
'opt' => $opt,
'pemangkasan' => $this->enumLabel(FrekuensiPemangkasanUtama::class, $practice->frekuensi_pemangkasan_utama ?? null, 'Belum ada data'),
'pemupukan' => $this->enumLabel(PemberianPupuk::class, $practice->pemberian_pupuk ?? null, 'Belum ada data'),
'praktik_baik' => $this->arrayValues($practice->kebiasaan_konservasi_tanah ?? null),
@@ -619,10 +695,10 @@ class SummaryController extends Controller
->all();
return [
'provinsi' => $baseLevels['provinsi'] ?? [],
'kabupaten' => $baseLevels['kabupaten'] ?? [],
'kecamatan' => $baseLevels['kecamatan'] ?? [],
'desa' => $baseLevels['desa'] ?? [],
'provinsi' => array_values($geoProvinsi),
'kabupaten' => array_values($geoKabupaten),
'kecamatan' => array_values($geoKecamatan),
'desa' => array_values($geoDesa),
'lahan' => $geoLahan,
];
}
@@ -826,7 +902,12 @@ class SummaryController extends Controller
return DB::table('akses_pasar')
->whereNull('akses_pasar.deleted_at')
->whereIn('akses_pasar.petani_id', $petaniIds)
->get(['praktik_fermentasi']);
->leftJoin('petani as akses_petani', 'akses_petani.id', '=', 'akses_pasar.petani_id')
->leftJoin('master_komoditas as akses_komoditas', 'akses_komoditas.id', '=', 'akses_petani.komoditas_id')
->get([
'akses_pasar.praktik_fermentasi',
'akses_komoditas.nama as komoditas_nama',
]);
}
private function filteredSurveyProduktivitas(Request $request)
@@ -1254,10 +1335,10 @@ class SummaryController extends Controller
];
}
private function fermentationPracticeChart($rows): array
private function fermentationPracticeByCommodityChart($rows): array
{
$labels = ['Ya, rutin', 'Pernah mencoba', 'Belum pernah'];
$counts = collect($labels)->mapWithKeys(fn ($label) => [$label => 0]);
$matrix = [];
foreach ($rows as $row) {
$label = $this->fermentationPracticeLabel($row->praktik_fermentasi ?? null);
@@ -1266,12 +1347,21 @@ class SummaryController extends Controller
continue;
}
$counts[$label] = (int) $counts[$label] + 1;
$commodity = $row->komoditas_nama ?: 'Belum ditentukan';
$matrix[$commodity] ??= array_fill_keys($labels, 0);
$matrix[$commodity][$label]++;
}
ksort($matrix, SORT_NATURAL | SORT_FLAG_CASE);
return [
'labels' => $labels,
'series' => collect($labels)->map(fn ($label) => (int) ($counts[$label] ?? 0))->values()->all(),
'labels' => array_keys($matrix),
'series' => collect($labels)
->map(fn ($label) => [
'name' => $label,
'data' => collect($matrix)->map(fn ($counts) => (int) $counts[$label])->values()->all(),
])
->all(),
];
}
@@ -23,7 +23,7 @@ use BackedEnum;
class SurveyPetaniController extends Controller
{
private const GAP_ENUM_CLASS = 'App\\Enums\\PendidikanPelatihan\\PelatihanGap';
private const GAP_ENUM_CLASS = 'App\\Enums\\PendidikanPelatihan\\PelatihanGAP';
public function index()
{
$colors = $this->colors();
@@ -31,7 +31,7 @@ class SurveyPetaniController extends Controller
// Progres per enumerator
$enumerators = User::query()
->where('role', 'fasilitator')
->where('role', 'enumerator')
->withCount([
'petaniDibuat as done',
])
@@ -24,7 +24,7 @@ class PetaniCPMController extends Controller
{
public function index(Request $request): View
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -60,7 +60,7 @@ class PetaniCPMController extends Controller
public function create(Request $request): View
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -94,7 +94,7 @@ class PetaniCPMController extends Controller
*/
public function store(Request $request): RedirectResponse
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -147,7 +147,7 @@ class PetaniCPMController extends Controller
*/
public function show(string $id): View
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -161,7 +161,7 @@ class PetaniCPMController extends Controller
*/
public function edit(Request $request, string $id): View
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -198,7 +198,7 @@ class PetaniCPMController extends Controller
*/
public function update(Request $request, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -251,7 +251,7 @@ class PetaniCPMController extends Controller
*/
public function destroy(string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -34,7 +34,7 @@ class ValueBasisDataController extends Controller
public function index(Request $request, string $tabelId): View
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Web\Konfigurasi;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* Mengatur komoditas yang ditangani seorang koordinator.
*/
class KoordinatorKomoditasController extends Controller
{
public function update(Request $request, User $user)
{
abort_unless(Gate::any(['super_admin', 'admin']), 403);
if (! $user->isKoordinator()) {
return response()->json([
'success' => false,
'message' => 'Komoditas hanya bisa diatur untuk user dengan role Koordinator.',
], 422);
}
$validated = $request->validate([
'komoditas_ids' => ['required', 'array', 'min:1'],
'komoditas_ids.*' => ['integer', 'exists:master_komoditas,id'],
], [
'komoditas_ids.required' => 'Pilih minimal satu komoditas.',
'komoditas_ids.min' => 'Pilih minimal satu komoditas.',
'komoditas_ids.*.exists' => 'Komoditas yang dipilih tidak valid.',
]);
self::simpanPenugasan($user, $validated['komoditas_ids']);
return response()->json([
'success' => true,
'message' => 'Komoditas koordinator berhasil diperbarui.',
'data' => $user->komoditasKoordinatorIds(),
]);
}
/**
* Menyamakan penugasan koordinator dengan daftar komoditas yang dipilih.
* Penugasan yang dilepas di-soft delete, penugasan baru dibuat.
*/
public static function simpanPenugasan(User $user, array $komoditasIds): void
{
$komoditasIds = array_values(array_unique(array_map('intval', $komoditasIds)));
DB::transaction(function () use ($user, $komoditasIds) {
$user->koordinatorKomoditas()
->whereNotIn('komoditas_id', $komoditasIds)
->get()
->each->delete();
$sudahAda = $user->komoditasKoordinatorIds();
foreach (array_diff($komoditasIds, $sudahAda) as $komoditasId) {
$user->koordinatorKomoditas()->create([
'komoditas_id' => $komoditasId,
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
}
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Web\Konfigurasi;
use App\Http\Controllers\Controller;
use App\Http\Requests\Konfigurasi\FilterLogAktivitasRequest;
use App\Models\Konfigurasi\LogAktivitas;
use App\Models\Pertanian\Petani;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class LogAktivitasController extends Controller
{
public const JENIS = [
'auth' => ['label' => 'Login', 'warna' => 'info'],
'petani' => ['label' => 'Profil Petani', 'warna' => 'primary'],
'verifikasi' => ['label' => 'Status Petani', 'warna' => 'success'],
'pertanian' => ['label' => 'Data Pertanian', 'warna' => 'primary'],
'map' => ['label' => 'Peta & Panen', 'warna' => 'primary'],
'user' => ['label' => 'Pengguna', 'warna' => 'warning'],
'profile' => ['label' => 'Profil Pengguna', 'warna' => 'warning'],
'konfigurasi' => ['label' => 'Konfigurasi', 'warna' => 'warning'],
'program' => ['label' => 'Program', 'warna' => 'danger'],
'core' => ['label' => 'Program (Inti)', 'warna' => 'danger'],
'bumdes' => ['label' => 'BUMDes', 'warna' => 'dark'],
'koperasi' => ['label' => 'Koperasi', 'warna' => 'dark'],
'ekosistem' => ['label' => 'Ekosistem', 'warna' => 'dark'],
'master' => ['label' => 'Master Data', 'warna' => 'secondary'],
'wilayah' => ['label' => 'Wilayah', 'warna' => 'secondary'],
'penduduk' => ['label' => 'Penduduk', 'warna' => 'secondary'],
'distribusi' => ['label' => 'Distribusi', 'warna' => 'secondary'],
'facilitator' => ['label' => 'Fasilitator', 'warna' => 'secondary'],
];
public const PER_PAGE = [25, 50, 100];
public function index(FilterLogAktivitasRequest $request)
{
$filter = $request->validated();
// Ambil log sesuai filter (tanpa COUNT total)
$aktivitas = LogAktivitas::query()
->with('causer:id,name,role')
->when($filter['jenis'] ?? null, fn ($q, $jenis) => $q->where('log_name', $jenis))
->when($filter['dari'] ?? null, fn ($q, $tgl) => $q->where('created_at', '>=', $tgl))
->when($filter['sampai'] ?? null, fn ($q, $tgl) => $q->where('created_at', '<', Carbon::parse($tgl)->addDay()))
->when($filter['search'] ?? null, fn ($q, $cari) => $this->cari($q, '%' . mb_strtolower($cari) . '%'))
->latest('id')
->simplePaginate((int) ($filter['per_page'] ?? self::PER_PAGE[0]))
->withQueryString();
// Nama petani untuk log yang belum menyimpan nama (satu query)
$idPetani = $aktivitas->getCollection()
->map(fn ($a) => $a->properties['petani_id'] ?? ($a->subject_type === Petani::class ? $a->subject_id : null))
->filter()
->unique();
$namaPetani = $idPetani->isEmpty() ? collect() : Petani::withoutGlobalScopes()->whereIn('id', $idPetani)->pluck('nama', 'id');
return view('konfigurasi.log-aktivitas.index', [
'aktivitas' => $aktivitas,
'jenis' => self::JENIS,
'perPage' => self::PER_PAGE,
'namaPetani' => $namaPetani,
]);
}
// Cari di aktivitas, nama pengguna, dan nama petani (memakai index)
private function cari(Builder $query, string $cari): void
{
$petani = Petani::withoutGlobalScopes()->select('id')->whereRaw('lower(nama) like ?', [$cari]);
$pengguna = User::withoutGlobalScopes()->select('id')->whereRaw('lower(name) like ?', [$cari]);
$query->where(fn ($q) => $q
->whereRaw('lower(description) like ?', [$cari])
->orWhere(fn ($q) => $q->where('causer_type', User::class)->whereIn('causer_id', $pengguna))
->orWhere(fn ($q) => $q->where('subject_type', Petani::class)->whereIn('subject_id', $petani))
->orWhereIn(DB::raw("(properties->>'petani_id')::bigint"), $petani));
}
}
@@ -8,16 +8,29 @@ use App\Models\User;
use App\Models\Profile;
use App\Enums\Role;
use App\Models\Wilayah\Provinsi;
use App\Models\Pertanian\Komoditas;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class UserController extends Controller
{
// Manajemen pengguna hanya untuk super admin & admin (sama dengan menu "Daftar Pengguna").
// Berlaku untuk semua method di controller ini.
public function __construct()
{
$this->middleware(function (Request $request, \Closure $next) {
abort_unless($request->user() && Gate::forUser($request->user())->any(['super_admin', 'admin']), 403);
return $next($request);
});
}
public function index(Request $request)
{
$query = User::orderBy('created_at', 'desc');
$query = User::with('koordinatorKomoditas.komoditas:id,nama')->orderBy('created_at', 'desc');
$tab = $request->get('tab', 'active');
if ($tab === 'pending') {
@@ -45,8 +58,15 @@ class UserController extends Controller
$pendingUsers = User::whereNull('email_verified_at')->count();
$roles = Role::cases();
$komoditasOptions = $this->komoditasOptions();
return view('konfigurasi.user.index', compact('users', 'totalUsers', 'activeUsers', 'pendingUsers', 'roles', 'tab'));
return view('konfigurasi.user.index', compact('users', 'totalUsers', 'activeUsers', 'pendingUsers', 'roles', 'tab', 'komoditasOptions'));
}
// Komoditas utama yang bisa ditugaskan ke koordinator
private function komoditasOptions()
{
return Komoditas::select('id', 'nama')->where('is_utama', true)->orderBy('nama')->get();
}
public function verify(Request $request, $id)
@@ -92,8 +112,9 @@ class UserController extends Controller
{
$roles = Role::cases();
$provinsi = Provinsi::orderBy('nama', 'asc')->get();
$komoditasOptions = $this->komoditasOptions();
return view('konfigurasi.user.add', compact('roles', 'provinsi'));
return view('konfigurasi.user.add', compact('roles', 'provinsi', 'komoditasOptions'));
}
public function store(Request $request)
@@ -104,6 +125,8 @@ class UserController extends Controller
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'role' => ['required', 'string'],
'komoditas_ids' => ['nullable', 'array', 'required_if:role,' . Role::KOORDINATOR->value],
'komoditas_ids.*' => ['integer', 'exists:master_komoditas,id'],
'telepon' => ['required', 'max:13'],
'desa_kelurahan_id' => ['nullable', 'exists:desa_kelurahan,id'],
'ktp' => ['nullable', 'min:16', 'max:16'],
@@ -116,6 +139,7 @@ class UserController extends Controller
'password.confirmed' => 'Konfirmasi password tidak cocok!',
'password.min' => 'Password minimal 8 karakter!',
'email.unique' => 'Email sudah terdaftar.',
'komoditas_ids.required_if' => 'Pilih minimal satu komoditas untuk koordinator.',
'required' => 'Kolom :attribute wajib diisi.'
]);
@@ -163,6 +187,10 @@ class UserController extends Controller
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
if ($user->isKoordinator()) {
KoordinatorKomoditasController::simpanPenugasan($user, $request->input('komoditas_ids', []));
}
});
if ($request->expectsJson()) {
@@ -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),
]);
}
}
@@ -24,7 +24,7 @@ class AksesPasarController extends Controller
{
$petani = Petani::findOrFail($petaniId);
$aksesPasar = AksesPasar::where('petani_id', $petaniId)->first();
$aksesPasar = AksesPasar::where('petani_id', $petaniId)->latest()->first();
$hargaJualPanen = HargaJualPanen::where('petani_id', $petaniId)->get();
$hargaJualRows = collect([
'Buah segar hasil petik',
@@ -50,32 +50,40 @@ class AksesPasarController extends Controller
}
public function store(Request $request)
{
$validated = $this->validateData($request);
{
$validated = $this->validateData($request);
DB::beginTransaction();
DB::beginTransaction();
try {
$aksesPasarData = $this->aksesPasarPayload($validated);
try {
$aksesPasarData = $this->aksesPasarPayload($validated);
$existingAksesPasar = AksesPasar::where('petani_id', $validated['petani_id'])->first();
if ($existingAksesPasar) {
$aksesPasarData['updated_by'] = Auth::id();
$existingAksesPasar->update($aksesPasarData);
$aksesPasar = $existingAksesPasar;
} else {
$aksesPasarData['uid'] = 'AKP-' . Str::uuid();
$aksesPasarData['created_by'] = Auth::id();
$aksesPasarData['updated_by'] = Auth::id();
$aksesPasar = AksesPasar::create($aksesPasarData);
if ($request->has('harga_jual')) {
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
}
DB::commit();
return redirect()->back()->with('success', 'Data Rantai Nilai dan Akses Pasar berhasil disimpan.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->withErrors(['error' => 'Gagal menyimpan data: ' . $e->getMessage()])->withInput();
}
if ($request->has('harga_jual')) {
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
}
DB::commit();
return redirect()->back()->with('success', 'Data Rantai Nilai dan Akses Pasar berhasil disimpan.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()->withErrors(['error' => 'Gagal menyimpan data: ' . $e->getMessage()])->withInput();
}
}
public function update(Request $request, $petaniId, $id)
{
@@ -118,7 +126,8 @@ class AksesPasarController extends Controller
'skorPengalamanBerhentiValues' => [
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
]
],
'sumberInfoLainnyaValue' => 'Lainnya',
];
}
@@ -128,6 +137,17 @@ class AksesPasarController extends Controller
return $request->validate([
'petani_id' => ['required', 'exists:petani,id'],
'tujuan_penjualan' => ['nullable', 'array'],
'tujuan_penjualan.*' => ['string'],
'tujuan_penjualan_lainnya' => [
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('tujuan_penjualan', []), true)),
'nullable', 'string', 'max:255'
],
'sifat_hubungan_bisnis' => ['nullable', 'string', 'max:255'],
'bentuk_penjualan' => ['nullable', 'string', 'max:255'],
'frekuensi_penjualan' => ['nullable', 'string', 'max:255'],
'skor_pengalaman' => ['nullable', 'integer', Rule::in(SkorPengalaman::values())],
'alasan_berhenti' => [
Rule::requiredIf(fn () => in_array((int)$request->input('skor_pengalaman'), [
@@ -144,6 +164,14 @@ class AksesPasarController extends Controller
'nullable', 'string', 'max:255'
],
'tahu_harga_sebelumnya' => ['nullable', 'string', 'max:255'],
'sumber_info_harga' => ['nullable', 'array'],
'sumber_info_harga.*' => ['string'],
'sumber_info_harga_lainnya' => [
Rule::requiredIf(fn () => in_array('Lainnya', $request->input('sumber_info_harga', []), true)),
'nullable', 'string', 'max:255'
],
'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())],
'proses_fermentasi' => [
Rule::requiredIf(fn () => filled($request->input('praktik_fermentasi')) && $request->input('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value),
@@ -168,6 +196,7 @@ class AksesPasarController extends Controller
], [
'petani_id.required' => 'Petani harus dipilih.',
'petani_id.exists' => 'Petani tidak ditemukan.',
'tujuan_penjualan_lainnya.required' => 'Keterangan saluran penjualan lainnya harus diisi.',
'skor_pengalaman.integer' => 'Skor pengalaman harus berupa angka.',
'skor_pengalaman.in' => 'Skor pengalaman tidak valid.',
'alasan_berhenti.required' => 'Alasan berhenti harus diisi.',
@@ -204,6 +233,10 @@ class AksesPasarController extends Controller
{
$payload = collect($validated)->except(['harga_jual'])->all();
if (!in_array('Lainnya', $payload['tujuan_penjualan'] ?? [], true)) {
$payload['tujuan_penjualan_lainnya'] = null;
}
if (!in_array((int)($payload['skor_pengalaman'] ?? 0), [
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
@@ -215,6 +248,10 @@ class AksesPasarController extends Controller
$payload['saluran_terdahulu_lainnya'] = null;
}
if (!in_array('Lainnya', $payload['sumber_info_harga'] ?? [], true)) {
$payload['sumber_info_harga_lainnya'] = null;
}
if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) {
$payload['proses_fermentasi'] = null;
$payload['model_fermentasi_memungkinkan'] = null;
@@ -223,7 +260,6 @@ class AksesPasarController extends Controller
return $payload;
}
private function syncHargaJual($petaniId, array $hargaJualData, $createdBy, $updatedBy = null): void
{
HargaJualPanen::where('petani_id', $petaniId)->delete();
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
@@ -26,26 +27,35 @@ class AsetKelompokTaniController extends Controller
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
}
private function redirectToSection(string $url, string $section, string $message)
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{
return redirect($url . '#section-' . $section)->with([
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message,
'success_section' => $section,
]);
}
private function kegiatanUrl(KelompokTani $kelompokTani): string
{
return route('kelompok-tani.edit', $kelompokTani->id) . '?section=aktivitas-fungsi';
}
private function nextAssetSection(KelompokTani $kelompokTani): string
{
return $kelompokTani->aset()->where('jenis_aset', 'Kas/simpanan kelompok')->where('status_aset', true)->exists()
? 'kas' : 'status-bantuan';
}
public function index(KelompokTani $kelompokTani)
{
$this->authorizeAccess();
$kelompokTani->load('aset');
return view('produktivitas.survey-kelompok-tani.aset-keuangan', compact('kelompokTani'));
return redirect($this->kegiatanUrl($kelompokTani) . '#section-aset-baku');
}
public function update(Request $request, KelompokTani $kelompokTani)
@@ -56,11 +66,26 @@ class AsetKelompokTaniController extends Controller
'aset' => ['required', 'array'],
'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)],
'aset.*.status_aset' => ['nullable', 'boolean'],
'aset.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
'aset.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
'aset.*.status_pengelolaan_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
'aset.*.kondisi_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
...$this->asetLainnyaRules(),
], [
'aset.*.status_pengelolaan_aset.required_if_accepted' => 'Status Pengelolaan wajib diisi jika aset dipilih Ada.',
'aset.*.kondisi_aset.required_if_accepted' => 'Kondisi wajib diisi jika aset dipilih Ada.',
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
]);
foreach ($validated['aset'] as $item) {
DB::transaction(function () use ($kelompokTani, $validated) {
$this->saveAsetBaku($kelompokTani, $validated['aset']);
$this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []);
});
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-baku', 'Data Operasional berhasil disimpan.', $this->nextAssetSection($kelompokTani));
}
private function saveAsetBaku(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$jenisAset = $item['jenis_aset'];
$ada = (bool) ($item['status_aset'] ?? false);
@@ -82,8 +107,8 @@ class AsetKelompokTaniController extends Controller
$existing->update([
'status_aset' => true,
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? $existing->status_pengelolaan_aset,
'kondisi_aset' => $item['kondisi_aset'] ?? $existing->kondisi_aset,
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
'kondisi_aset' => $item['kondisi_aset'],
'deleted_by' => null,
'updated_by' => auth()->id(),
]);
@@ -94,30 +119,40 @@ class AsetKelompokTaniController extends Controller
'uid' => 'AKT-' . strtoupper(Str::random(10)),
'jenis_aset' => $jenisAset,
'status_aset' => true,
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? '-',
'kondisi_aset' => $item['kondisi_aset'] ?? '-',
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
'kondisi_aset' => $item['kondisi_aset'],
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
}
}
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-baku', 'Data aset produktif berhasil disimpan.');
private function asetLainnyaRules(): array
{
return [
'aset_lainnya' => ['nullable', 'array'],
'aset_lainnya.*.jenis_aset' => ['nullable', 'string', 'max:255', Rule::notIn(self::JENIS_ASET_BAKU)],
'aset_lainnya.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
'aset_lainnya.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
];
}
public function store(Request $request, KelompokTani $kelompokTani)
{
$this->authorizeAccess();
$validated = $request->validate([
'aset_lainnya' => ['nullable', 'array'],
'aset_lainnya.*.jenis_aset' => ['nullable', 'string', 'max:255', Rule::notIn(self::JENIS_ASET_BAKU)],
'aset_lainnya.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
'aset_lainnya.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
], [
$validated = $request->validate($this->asetLainnyaRules(), [
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
]);
foreach ($validated['aset_lainnya'] ?? [] as $item) {
DB::transaction(fn () => $this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []));
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil ditambahkan.', $this->nextAssetSection($kelompokTani));
}
private function createAsetLainnya(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$jenisAset = trim($item['jenis_aset'] ?? '');
if ($jenisAset === '') {
@@ -134,8 +169,6 @@ class AsetKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
}
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil ditambahkan.');
}
public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset)
@@ -143,11 +176,11 @@ class AsetKelompokTaniController extends Controller
$this->authorizeAccess();
abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404);
abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Aset baku diatur lewat checklist, bukan dihapus langsung.');
abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Operasional: gunakan pilihan Ada/Tidak pada tabel untuk menghapus aset baku.');
$aset->update(['deleted_by' => auth()->id()]);
$aset->delete();
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil dihapus.');
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil dihapus.');
}
}
@@ -5,7 +5,9 @@ namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Models\Pertanian\KelompokTani;
use App\Models\Pertanian\BantuanKelompokTani;
@@ -14,14 +16,14 @@ class BantuanKelompokTaniController extends Controller
{
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
}
private function redirectToSection(string $url, string $section, string $message)
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{
return redirect($url . '#section-' . $section)->with([
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message,
'success_section' => $section,
]);
@@ -29,7 +31,7 @@ class BantuanKelompokTaniController extends Controller
private function redirectToSectionWithError(string $url, string $section, string $message)
{
return redirect($url . '#section-' . $section)->with([
return redirect($url . '#kelompok-tani-notifications')->with([
'error' => $message,
'error_section' => $section,
]);
@@ -39,9 +41,72 @@ class BantuanKelompokTaniController extends Controller
{
$this->authorizeAccess();
$kelompokTani->load('bantuan');
return redirect($this->operasionalUrl($kelompokTani) . '#section-status-bantuan');
}
return view('produktivitas.survey-kelompok-tani.bantuan-penilaian', compact('kelompokTani'));
private function operasionalUrl(KelompokTani $kelompokTani): string
{
return route('kelompok-tani.edit', $kelompokTani->id) . '?section=aktivitas-fungsi';
}
private function hasRecordedAid(KelompokTani $kelompokTani): bool
{
return $kelompokTani->bantuan()->whereRaw("TRIM(nama_program) <> ''")->exists();
}
private function bantuanRules(bool $pernah = true, bool $requireFirstRow = false): array
{
$exclude = Rule::excludeIf(!$pernah);
return [
'bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'array'],
'bantuan.*' => [$exclude, 'array'],
'bantuan.*.nama_program' => [$exclude, 'required_with:bantuan.*.penyelenggara,bantuan.*.bentuk_bantuan,bantuan.*.tahun', 'nullable', 'string', 'max:255'],
'bantuan.*.penyelenggara' => [$exclude, 'nullable', 'string', 'max:255'],
'bantuan.*.bentuk_bantuan' => [$exclude, 'nullable', 'string', 'max:255'],
'bantuan.*.tahun' => [$exclude, 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
'bantuan.0.nama_program' => [$exclude, Rule::requiredIf($requireFirstRow), 'required_with:bantuan.0.penyelenggara,bantuan.0.bentuk_bantuan,bantuan.0.tahun', 'nullable', 'string', 'max:255'],
'bantuan.0.penyelenggara' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
'bantuan.0.bentuk_bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
'bantuan.0.tahun' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
];
}
private function bantuanMessages(): array
{
return [
'pernah_mendapatkan_bantuan.required' => 'Pengalaman Akses Bantuan/Program wajib diisi.',
'pernah_mendapatkan_bantuan.boolean' => 'Pilih Pernah atau Belum Pernah untuk Pengalaman Akses Bantuan/Program.',
'bantuan.required' => 'Isi minimal satu data bantuan jika memilih Pernah.',
'bantuan.0.nama_program.required' => 'Nama Program/Bantuan pada baris pertama wajib diisi.',
'bantuan.0.penyelenggara.required' => 'Penyelenggara pada baris pertama wajib diisi.',
'bantuan.0.bentuk_bantuan.required' => 'Bentuk Bantuan pada baris pertama wajib diisi.',
'bantuan.0.tahun.required' => 'Tahun pada baris pertama wajib diisi.',
'bantuan.*.nama_program.required_with' => 'Nama Program/Bantuan wajib diisi jika baris bantuan diisi.',
];
}
public function update(Request $request, KelompokTani $kelompokTani)
{
$this->authorizeAccess();
$validated = $request->validate([
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
...$this->bantuanRules(
$request->boolean('pernah_mendapatkan_bantuan'),
$request->boolean('pernah_mendapatkan_bantuan') && !$this->hasRecordedAid($kelompokTani)
),
], $this->bantuanMessages());
DB::transaction(function () use ($kelompokTani, $validated) {
$kelompokTani->update([
'pernah_mendapatkan_bantuan' => $validated['pernah_mendapatkan_bantuan'],
'updated_by' => auth()->id(),
]);
$this->createBantuan($kelompokTani, $validated['bantuan'] ?? []);
});
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil disimpan.');
}
public function store(Request $request, KelompokTani $kelompokTani)
@@ -50,37 +115,37 @@ class BantuanKelompokTaniController extends Controller
if (!$kelompokTani->pernah_mendapatkan_bantuan) {
return $this->redirectToSectionWithError(
route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id),
$this->operasionalUrl($kelompokTani),
'status-bantuan',
'Set status jadi "Pernah" dulu sebelum menambahkan daftar bantuan/program.'
'Operasional: pilih jawaban "Pernah" terlebih dahulu sebelum menambahkan data bantuan/program.'
);
}
$validated = $request->validate([
'bantuan' => ['nullable', 'array'],
'bantuan.*.nama_program' => ['nullable', 'string', 'max:255'],
'bantuan.*.penyelenggara' => ['nullable', 'string', 'max:255'],
'bantuan.*.bentuk_bantuan' => ['nullable', 'string', 'max:255'],
'bantuan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
]);
$validated = $request->validate($this->bantuanRules(true, !$this->hasRecordedAid($kelompokTani)), $this->bantuanMessages());
foreach ($validated['bantuan'] ?? [] as $item) {
if (empty($item['nama_program']) && empty($item['penyelenggara']) && empty($item['bentuk_bantuan'])) {
DB::transaction(fn () => $this->createBantuan($kelompokTani, $validated['bantuan'] ?? []));
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil ditambahkan.');
}
private function createBantuan(KelompokTani $kelompokTani, array $items): void
{
foreach ($items as $item) {
$namaProgram = trim($item['nama_program'] ?? '');
if ($namaProgram === '') {
continue;
}
$kelompokTani->bantuan()->create([
'uid' => 'BKT-' . strtoupper(Str::random(10)),
'nama_program' => $item['nama_program'] ?? null,
'penyelenggara' => $item['penyelenggara'] ?? null,
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? null,
'nama_program' => $namaProgram,
'penyelenggara' => $item['penyelenggara'] ?? '',
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? '',
'tahun' => $item['tahun'] ?? null,
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
}
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil ditambahkan.');
}
public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan)
@@ -89,9 +154,18 @@ class BantuanKelompokTaniController extends Controller
abort_unless($bantuan->survey_kelompok_tani_id === $kelompokTani->id, 404);
if ($kelompokTani->pernah_mendapatkan_bantuan && !$kelompokTani->bantuan()
->where('id', '!=', $bantuan->id)->whereRaw("TRIM(nama_program) <> ''")->exists()) {
return $this->redirectToSectionWithError(
$this->operasionalUrl($kelompokTani),
'status-bantuan',
'Operasional: minimal satu data bantuan harus tersedia saat jawaban masih Pernah.'
);
}
$bantuan->update(['deleted_by' => auth()->id()]);
$bantuan->delete();
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil dihapus.');
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil dihapus.');
}
}
@@ -6,7 +6,9 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Enums\PendidikanPelatihan\PelatihanGAP;
use App\Models\Pertanian\Petani;
use App\Models\Pertanian\SurveyPelatihan;
use App\Models\Pertanian\SurveySertifikasi;
@@ -15,7 +17,7 @@ class PendidikanPelatihanController extends Controller
{
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
}
@@ -44,15 +46,22 @@ class PendidikanPelatihanController extends Controller
$validatedData = $request->validate([
'riwayat_pelatihan' => ['nullable', 'array'],
'riwayat_pelatihan.*.jenis_pelatihan' => ['nullable', 'string', 'max:255'],
'riwayat_pelatihan.*.jenis_pelatihan' => ['nullable', 'string', Rule::enum(PelatihanGAP::class)],
'riwayat_pelatihan.*.jenis_pelatihan_lainnya' => [
'nullable', 'string', 'max:255',
'required_if:riwayat_pelatihan.*.jenis_pelatihan,' . PelatihanGAP::LAINNYA->value,
],
'riwayat_pelatihan.*.penyelenggara' => ['nullable', 'string', 'max:255'],
'riwayat_pelatihan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
], [
'riwayat_pelatihan.*.jenis_pelatihan.enum' => 'Jenis pelatihan yang dipilih tidak valid.',
'riwayat_pelatihan.*.jenis_pelatihan_lainnya.required_if' => 'Jenis pelatihan lainnya wajib diisi jika memilih Lainnya.',
'riwayat_pelatihan.*.tahun.integer' => 'Tahun pelatihan harus berupa angka.',
'riwayat_pelatihan.*.tahun.min' => 'Tahun pelatihan tidak valid.',
'riwayat_pelatihan.*.tahun.max' => 'Tahun pelatihan tidak boleh melebihi tahun saat ini.',
], [
'riwayat_pelatihan.*.jenis_pelatihan' => 'jenis pelatihan',
'riwayat_pelatihan.*.jenis_pelatihan_lainnya' => 'jenis pelatihan lainnya',
'riwayat_pelatihan.*.penyelenggara' => 'penyelenggara',
'riwayat_pelatihan.*.tahun' => 'tahun pelatihan',
]);
@@ -66,10 +75,15 @@ class PendidikanPelatihanController extends Controller
continue;
}
// Jika memilih "Lainnya", yang disimpan adalah teks isian bebasnya
$jenisPelatihan = ($item['jenis_pelatihan'] ?? null) === PelatihanGAP::LAINNYA->value
? trim($item['jenis_pelatihan_lainnya'])
: ($item['jenis_pelatihan'] ?? null);
$petani->surveyPelatihan()->create([
'uid' => 'SPL-' . strtoupper(Str::random(10)),
'petani_id' => $petani->id,
'jenis_pelatihan' => $item['jenis_pelatihan'] ?? null,
'jenis_pelatihan' => $jenisPelatihan,
'penyelenggara' => $item['penyelenggara'] ?? null,
'tahun_pelaksanaan' => $item['tahun'] ?? null,
'created_by' => auth()->id(),
@@ -14,7 +14,7 @@ class PengurusKelompokTaniController extends Controller
{
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
}
@@ -31,9 +31,7 @@ class PengurusKelompokTaniController extends Controller
{
$this->authorizeAccess();
$kelompokTani->load('pengurus');
return view('produktivitas.survey-kelompok-tani.pengurus-anggota', compact('kelompokTani'));
return redirect(route('kelompok-tani.edit', $kelompokTani->id) . '#section-ringkasan');
}
public function store(Request $request, KelompokTani $kelompokTani)
@@ -64,7 +62,7 @@ class PengurusKelompokTaniController extends Controller
]);
}
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil ditambahkan.');
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil ditambahkan.');
}
public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus)
@@ -76,6 +74,6 @@ class PengurusKelompokTaniController extends Controller
$pengurus->update(['deleted_by' => auth()->id()]);
$pengurus->delete();
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil dihapus.');
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil dihapus.');
}
}
@@ -32,13 +32,13 @@ class PetaniController extends Controller
*/
public function index(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
$provinsi = Provinsi::select('id', 'kode', 'nama')->orderBy('kode', 'asc')->get();
$userRole = auth()->user()->role->value;
$showFasilitatorColumn = in_array($userRole, ['super_admin', 'admin'], true);
$showFasilitatorColumn = in_array($userRole, ['super_admin', 'admin', 'koordinator'], true);
$defaultKabupaten = KabupatenKota::select('id', 'nama')
->whereRaw('lower(nama) like ?', ['%ngada%'])
->orderBy('nama')
@@ -102,10 +102,19 @@ class PetaniController extends Controller
$query->where('desa_kelurahan_id', $request->desa_kelurahan_id);
}
if ($userRole === 'fasilitator') {
if ($userRole === 'enumerator') {
$query->where('created_by', auth()->user()->id);
}
// Koordinator hanya melihat petani dengan komoditas yang ditugaskan
$komoditasKoordinatorIds = $userRole === 'koordinator'
? auth()->user()->komoditasKoordinatorIds()
: null;
if ($komoditasKoordinatorIds !== null) {
$query->whereIn('komoditas_id', $komoditasKoordinatorIds);
}
$totalPetani = (clone $query)->count();
$totalPetaniLakiLaki = (clone $query)->where('jenis_kelamin', 1)->count();
$totalPetaniPerempuan = (clone $query)->where('jenis_kelamin', 0)->count();
@@ -165,10 +174,15 @@ class PetaniController extends Controller
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
});
})
->when($userRole === 'fasilitator', function ($query) {
->when($userRole === 'enumerator', function ($query) {
$query->whereHas('petani', function ($petani) {
$petani->where('created_by', auth()->id());
});
})
->when($komoditasKoordinatorIds !== null, function ($query) use ($komoditasKoordinatorIds) {
$query->whereHas('petani', function ($petani) use ($komoditasKoordinatorIds) {
$petani->whereIn('komoditas_id', $komoditasKoordinatorIds);
});
});
$totalLahan = (clone $lahanSummaryQuery)->count();
@@ -216,7 +230,7 @@ class PetaniController extends Controller
public function create()
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -288,7 +302,7 @@ class PetaniController extends Controller
public function store(Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -320,7 +334,7 @@ class PetaniController extends Controller
public function show(string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -332,7 +346,7 @@ class PetaniController extends Controller
public function edit(string $id, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -353,7 +367,7 @@ class PetaniController extends Controller
public function update(Request $request, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -395,14 +409,14 @@ class PetaniController extends Controller
public function destroy(string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
}
try {
$petani = Petani::findOrFail($id);
if (auth()->user()->role->value === 'fasilitator' && $petani->created_by !== auth()->id()) {
if (auth()->user()->role->value === 'enumerator' && $petani->created_by !== auth()->id()) {
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
}
@@ -429,7 +443,7 @@ class PetaniController extends Controller
public function showImage(string $id, string $field)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -32,7 +32,7 @@ class PetaniLahanController extends Controller
{
public function index($petaniId, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -52,7 +52,7 @@ class PetaniLahanController extends Controller
public function create($petaniId)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -194,7 +194,7 @@ class PetaniLahanController extends Controller
public function store($petaniId, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -292,7 +292,7 @@ class PetaniLahanController extends Controller
public function show(string $petaniId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -310,7 +310,7 @@ class PetaniLahanController extends Controller
public function edit(string $petaniId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -335,7 +335,7 @@ class PetaniLahanController extends Controller
public function update(Request $request, string $petaniId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -462,7 +462,7 @@ class PetaniLahanController extends Controller
public function destroy(string $petaniId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -481,7 +481,7 @@ class PetaniLahanController extends Controller
public function summary(string $petaniId)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -42,7 +42,7 @@ class PetaniLahanTanamanController extends Controller
*/
public function show(string $petaniId, string $lahanId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -63,7 +63,7 @@ class PetaniLahanTanamanController extends Controller
public function update(Request $request, string $petaniId, string $lahanId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -103,7 +103,7 @@ class PetaniLahanTanamanController extends Controller
*/
public function destroy(string $petaniId, string $lahanId, string $id)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Web\Produktivitas;
use App\Enums\StatusDataPetani;
use App\Http\Controllers\Controller;
use App\Models\Pertanian\Petani;
use App\Services\Produktivitas\KelengkapanPetani;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* Enumerator penginput menandai data petani Lengkap, atau membatalkannya kembali ke Draf.
* Perubahan ke Terverifikasi dilakukan koordinator (menu menyusul), bukan di sini.
*/
class PetaniStatusController extends Controller
{
public function update(Request $request, Petani $petani)
{
abort_unless($petani->bisaDitandaiOleh($request->user()), 403);
$validated = $request->validate([
'status' => ['required', Rule::in([StatusDataPetani::LENGKAP->value, StatusDataPetani::DRAFT->value])],
// Enumerator menyetujui pernyataan kelengkapan (tombol konfirmasi di popup) saat menandai lengkap.
'pernyataan' => ['exclude_unless:status,' . StatusDataPetani::LENGKAP->value, 'accepted'],
], [
'pernyataan.accepted' => 'Setujui pernyataan kelengkapan data terlebih dahulu.',
]);
$tujuan = StatusDataPetani::from($validated['status']);
if ($petani->status === StatusDataPetani::TERVERIFIKASI) {
return back()->with('status_petani_error', 'Data petani sudah terverifikasi. Hubungi koordinator untuk mengubahnya.');
}
if ($tujuan === StatusDataPetani::LENGKAP) {
if ($petani->status !== StatusDataPetani::DRAFT) {
return back()->with('status_petani_error', 'Data petani sudah ditandai lengkap.');
}
$wajibKosong = KelengkapanPetani::untuk($petani)->jumlahWajibKosong();
if ($wajibKosong > 0) {
return back()->with('status_petani_error', "Masih ada {$wajibKosong} isian wajib yang kosong.");
}
}
if ($tujuan === StatusDataPetani::DRAFT && $petani->status !== StatusDataPetani::LENGKAP) {
return back()->with('status_petani_error', 'Data petani masih berstatus draf.');
}
$dari = $petani->status;
$petani->status = $tujuan;
$petani->updated_by = $request->user()->id;
$petani->save();
$petani->catatPerubahanStatus($dari, $tujuan === StatusDataPetani::LENGKAP
? 'Ditandai lengkap oleh enumerator'
: 'Tanda lengkap dibatalkan oleh enumerator');
return back()->with('status_petani_success', $tujuan === StatusDataPetani::LENGKAP
? 'Data petani berhasil ditandai lengkap.'
: 'Tanda lengkap berhasil dibatalkan. Status kembali ke draf.');
}
}
@@ -42,7 +42,7 @@ class PraktikBudidayaController extends Controller
{
public function index($petaniId, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -139,7 +139,7 @@ class PraktikBudidayaController extends Controller
public function store($petaniId, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -184,6 +184,12 @@ class PraktikBudidayaController extends Controller
$petani = Petani::findOrFail($petaniId);
if (!$petani->isKomoditasVanili()) {
$validatedData['penyerbukan_manual'] = null;
$validatedData['alasan_tidak_penyerbukan'] = null;
$validatedData['tingkat_keberhasilan_penyerbukan'] = null;
}
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
$praktikBudidaya = $petani->praktikBudidaya()->create([
@@ -241,7 +247,7 @@ class PraktikBudidayaController extends Controller
public function edit($petaniId, $praktikBudidayaId)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -334,7 +340,7 @@ class PraktikBudidayaController extends Controller
public function update($petaniId, $praktikBudidayaId, Request $request)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -567,6 +573,12 @@ class PraktikBudidayaController extends Controller
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
if (!$petani->isKomoditasVanili()) {
$validatedData['penyerbukan_manual'] = null;
$validatedData['alasan_tidak_penyerbukan'] = null;
$validatedData['tingkat_keberhasilan_penyerbukan'] = null;
}
$dataPraktikBudidaya = [
'tenaga_kerja' => $validatedData['tenaga_kerja'],
'frekuensi_pemangkasan_utama' => $validatedData['frekuensi_pemangkasan_komoditas_utama'],
@@ -682,7 +694,7 @@ class PraktikBudidayaController extends Controller
'uid' => 'SP-' . strtoupper(Str::random(10)),
'petani_id' => $petani->id,
'created_by' => auth()->id(),
'updated_by_by' => auth()->id()
'updated_by' => auth()->id()
], $dataSurveyPanen));
}
}
@@ -49,20 +49,43 @@ class ProfilPetaniController extends Controller
$skorPetani = SkorPetani::where('petani_id', $id)->first();
$enumerator = Profile::where('user_id', $petani->created_by)->first();
$komoditasUtama = Komoditas::where('id', $petani->komoditas_id)->first();
$lahan = Lahan::where('petani_id', $id )->get();
$lahan = Lahan::query()
->select(['id', 'petani_id', 'nama', 'path', 'luas_lahan', 'komoditas_id', 'desa_kelurahan_id'])
->with([
'komoditas:id,nama',
'desaKelurahan:id,nama,path,kecamatan_id',
'desaKelurahan.kecamatan:id,nama,path',
])
->where('petani_id', $id)
->get();
$desaKelurahan = DesaKelurahan::with('kecamatan')
->where('id', $petani->desa_kelurahan_id)
->first();
$ktpMasked = $petani->ktp ? Str::mask($petani->ktp, '*', 4, -4) : '-';
$profiling = $skorPetani ? $this->buildProfiling($skorPetani) : null;
$assessmentFields = [
'skor_kesiapan_adopsi',
'skor_d2_sistem_praktik_budidaya',
'skor_d4_sosial_ekonomi',
'skor_d6_rantai_nilai_akses_pasar',
'skor_komp_1_d4_k3_motivasi_kapasitas_adopsi',
'skor_komp_2_d4_k2_ekonomi_rumah_tangga',
'skor_komp_3_d2_praktik_budidaya_gap',
'skor_komp_4_d4_k4_keterlibatan_kelembagaan',
'skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan',
'skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar',
];
$hasAssessment = $skorPetani !== null && collect($assessmentFields)->every(
fn (string $field) => $skorPetani->{$field} !== null
);
$profiling = $hasAssessment ? $this->buildProfiling($skorPetani) : null;
$tanaman = Tanaman::where('petani_id', $petani->id)
->where('komoditas_id', $petani->komoditas_id)
->first();
if ($tanaman) {
if ($tanaman?->updated_at && $tanaman->umur_tanaman !== null) {
$tahunInputTanaman = $tanaman->updated_at->year;
$umurTanaman = (int) $tanaman->umur_tanaman;
$tahunSekarang = now()->year;
@@ -162,8 +185,8 @@ class ProfilPetaniController extends Controller
$d4SosialEkonomi = null;
$terdaftarKelompok = $sosialEkonomi->terdaftar_kelompok_tani;
$peranKelompok = $sosialEkonomi->peran_dalam_kelompok;
$terdaftarKelompok = $sosialEkonomi?->terdaftar_kelompok_tani;
$peranKelompok = $sosialEkonomi?->peran_dalam_kelompok;
if ($sosialEkonomi) {
@@ -207,8 +230,8 @@ class ProfilPetaniController extends Controller
'icon' => 'tabler-minus',
],
default => [
'label' => Str::headline($status),
'short' => Str::headline($status),
'label' => filled($status) ? Str::headline($status) : 'Belum diisi',
'short' => filled($status) ? Str::headline($status) : 'Belum diisi',
'color' => 'secondary',
'icon' => 'tabler-help-circle',
],
@@ -434,12 +457,61 @@ class ProfilPetaniController extends Controller
],
];
// Report missing profile data without treating an unanswered field as zero.
$missingSections = [];
$identityFields = [
'nama' => 'Nama', 'ktp' => 'NIK', 'tanggal_lahir' => 'Tanggal lahir',
'jenis_kelamin' => 'Jenis kelamin', 'pendidikan_terakhir' => 'Pendidikan terakhir',
'status_perkawinan' => 'Status perkawinan',
];
foreach ($identityFields as $field => $label) {
if (blank($petani->{$field})) {
$missingSections[] = $label;
}
}
if (!$desaKelurahan?->nama || !$desaKelurahan?->kecamatan?->nama) {
$missingSections[] = 'Wilayah desa/kecamatan';
}
if (!$komoditasUtama?->nama) {
$missingSections[] = 'Komoditas utama';
}
if ($lahan->isEmpty()) {
$missingSections[] = 'Data lahan';
}
if ($tahunTanam === null) {
$missingSections[] = 'Umur tanaman';
}
if (!$praktikBudidaya || $indikatorOnFarm->contains(fn ($item) => $item['skor'] === null)) {
$missingSections[] = 'Praktik budidaya';
}
if ($surveyPanen?->hasil_komoditas_utama_permusim === null) {
$missingSections[] = 'Survei panen';
}
$socialFields = [
'jumlah_tanggungan', 'skor_kecukupan_penghasilan', 'skor_kemampuan_menabung',
'status_kepemilikan_lahan', 'terdaftar_kelompok_tani', 'status_rekening_tabungan',
'pengalaman_kredit_kebun', 'keterikatan_hutang_panen', 'praktik_ijon',
'kebebasan_memilih_pembeli',
];
if (!$sosialEkonomi || collect($socialFields)->contains(fn ($field) => blank($sosialEkonomi->{$field}))) {
$missingSections[] = 'Sosial ekonomi';
}
if (!$aksesPasar || blank($aksesPasar->saluran_terdahulu)) {
$missingSections[] = 'Akses pasar';
}
if (!$hasAssessment) {
$missingSections[] = 'Penilaian kesiapan adopsi belum lengkap';
}
$data = [
'missingSections' => $missingSections,
'hasAssessment' => $hasAssessment,
'petani' => $petani,
'skorPetani' => $skorPetani,
'enumerator' => $enumerator,
'komoditasUtama' => $komoditasUtama,
'lahan' => $lahan,
'farmerLandMap' => $this->farmerLandMapData($lahan),
'desaKelurahan' => $desaKelurahan,
'ktpMasked' => $ktpMasked,
'profiling' => $profiling,
@@ -447,7 +519,6 @@ class ProfilPetaniController extends Controller
'umurTanaman' => $umurTanaman,
'surveyPanen' => $surveyPanen,
'praktikBudidaya' => $praktikBudidaya,
'praktikBudidaya' => $praktikBudidaya,
'indikatorOnFarm' => $indikatorOnFarm,
'detailOnFarm' => $detailOnFarm,
'sosialEkonomi' => $sosialEkonomi,
@@ -464,6 +535,35 @@ class ProfilPetaniController extends Controller
return view('produktivitas.profil-petani.index', $data);
}
private function farmerLandMapData(\Illuminate\Support\Collection $lands): array
{
$regions = ['desa' => [], 'kecamatan' => []];
$plots = [];
foreach ($lands as $land) {
$plot = [
'id' => $land->id,
'nama' => $land->nama,
'path' => $land->path,
'luas' => $land->luas_lahan,
'komoditas' => $land->komoditas?->nama,
];
foreach (['desa' => $land->desaKelurahan, 'kecamatan' => $land->desaKelurahan?->kecamatan] as $level => $region) {
$plot[$level . '_id'] = $region?->id;
if ($region && !isset($regions[$level][$region->id])) {
$regions[$level][$region->id] = [
'id' => $region->id,
'nama' => $region->nama,
'path' => $region->path,
];
}
}
$plots[] = $plot;
}
return ['lands' => $plots, 'regions' => $regions];
}
private function getScoreColor(?int $score): string
{
if ($score === null) {
@@ -16,7 +16,7 @@ class SkorPetaniController extends Controller
*/
public function index(string $petaniId)
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
abort(403);
}
@@ -3,42 +3,42 @@
namespace App\Http\Controllers\Web\Produktivitas;
use App\Http\Controllers\Controller;
use App\Http\Requests\Produktivitas\IndexKelompokTaniRequest;
use App\Services\Produktivitas\KelompokTaniService;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Models\Pertanian\KelompokTani;
use App\Models\Wilayah\Provinsi;
use App\Models\Wilayah\KabupatenKota;
use App\Models\Wilayah\Kecamatan;
use App\Models\Wilayah\DesaKelurahan;
use App\Models\Pertanian\Komoditas;
class SurveyKelompokTaniController extends Controller
{
private function authorizeAccess(): void
{
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
abort(403);
}
}
private function redirectToSection(string $url, string $section, string $message)
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
{
return redirect($url . '#section-' . $section)->with([
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
'success' => $message,
'success_section' => $section,
]);
}
public function index(Request $request)
public function index(IndexKelompokTaniRequest $request): View
{
$kelompokTani = KelompokTani::query()
->when($request->search, fn ($query, $search) => $query->where('nama_kelompok', 'like', "%{$search}%"))
->latest()
->paginate($request->per_page ?? 10)
->appends($request->query());
return view('produktivitas.survey-kelompok-tani.index', compact('kelompokTani'));
return view('produktivitas.survey-kelompok-tani.index', app(KelompokTaniService::class)->indexData($request->validated()));
}
public function create()
{
$this->authorizeAccess();
@@ -46,7 +46,25 @@ class SurveyKelompokTaniController extends Controller
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
$komoditas = Komoditas::where('is_utama', true)->get();
return view('produktivitas.survey-kelompok-tani.add', compact('provinsi', 'komoditas'));
return view('produktivitas.survey-kelompok-tani.add', [
...compact('provinsi', 'komoditas'),
'ringkasanProfil' => app(KelompokTaniService::class)->profileSummary(),
...$this->profileLocationOptions(),
]);
}
private function profileLocationOptions(?KelompokTani $kelompokTani = null): array
{
$locationId = static fn ($value) => is_scalar($value) && filter_var($value, FILTER_VALIDATE_INT) !== false ? (int) $value : null;
$provinsiId = $locationId(old('provinsi_id', $kelompokTani?->desaKelurahan?->kecamatan?->kabupatenKota?->provinsi_id));
$kabupatenId = $locationId(old('kabupaten_id', $kelompokTani?->desaKelurahan?->kecamatan?->kabupaten_kota_id));
$kecamatanId = $locationId(old('kecamatan_id', $kelompokTani?->desaKelurahan?->kecamatan_id));
return [
'kabupatenOptions' => filled($provinsiId) ? KabupatenKota::where('provinsi_id', $provinsiId)->orderBy('nama')->get() : collect(),
'kecamatanOptions' => filled($kabupatenId) ? Kecamatan::where('kabupaten_kota_id', $kabupatenId)->orderBy('nama')->get() : collect(),
'desaOptions' => filled($kecamatanId) ? DesaKelurahan::where('kecamatan_id', $kecamatanId)->orderBy('nama')->get() : collect(),
];
}
public function store(Request $request)
@@ -67,7 +85,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil ditambahkan.');
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil ditambahkan.', 'pengurus');
}
private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string
@@ -80,15 +98,21 @@ class SurveyKelompokTaniController extends Controller
public function edit(Request $request, KelompokTani $kelompokTani)
{
$this->authorizeAccess();
$ringkasanProfil = app(KelompokTaniService::class)->profileSummary($kelompokTani);
return match ($request->query('section', 'identitas')) {
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')),
'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', compact('kelompokTani')),
'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani')),
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani', 'ringkasanProfil')),
'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', [
'kelompokTani' => $kelompokTani->loadMissing(['aset', 'bantuan']),
'ringkasanProfil' => $ringkasanProfil,
]),
'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani', 'ringkasanProfil')),
default => view('produktivitas.survey-kelompok-tani.edit', [
'kelompokTani' => $kelompokTani,
'kelompokTani' => $kelompokTani->loadMissing('pengurus'),
'ringkasanProfil' => $ringkasanProfil,
'provinsi' => Provinsi::orderBy('kode', 'asc')->get(),
'komoditas' => Komoditas::where('is_utama', true)->get(),
...$this->profileLocationOptions($kelompokTani),
]),
};
}
@@ -103,8 +127,8 @@ class SurveyKelompokTaniController extends Controller
'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani),
'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani),
'sosial-konflik' => $this->updateSosialKonflik($request, $kelompokTani),
'status-bantuan' => $this->updateStatusBantuan($request, $kelompokTani),
'penilaian' => $this->updatePenilaian($request, $kelompokTani),
'status-bantuan' => app(BantuanKelompokTaniController::class)->update($request, $kelompokTani),
'penilaian' => redirect($this->editUrl($kelompokTani, 'aktivitas-fungsi') . '#section-status-bantuan'),
default => $this->updateIdentitas($request, $kelompokTani),
};
}
@@ -122,7 +146,7 @@ class SurveyKelompokTaniController extends Controller
private function validateIdentitas(Request $request)
{
return $request->validate([
$validated = $request->validate([
// Data Responden
'nama_responden' => ['required', 'string', 'max:255'],
'jabatan_responden' => ['nullable', 'string', 'max:255'],
@@ -130,7 +154,10 @@ class SurveyKelompokTaniController extends Controller
// Identitas Kelompok Tani
'nama_kelompok' => ['required', 'string', 'max:255'],
'desa_kelurahan_id' => ['required', 'exists:master_desa_kelurahan,id'],
'provinsi_id' => ['bail', 'required', 'integer', 'exists:master_provinsi,id'],
'kabupaten_id' => ['bail', 'required', 'integer', 'exists:master_kabupaten_kota,id'],
'kecamatan_id' => ['bail', 'required', 'integer', 'exists:master_kecamatan,id'],
'desa_kelurahan_id' => ['bail', 'required', 'integer', 'exists:master_desa_kelurahan,id'],
'dusun' => ['nullable', 'string', 'max:255'],
'tahun_berdiri' => ['nullable', 'digits:4', 'integer', 'min:1900', 'max:' . date('Y')],
'jenis_kelompok' => ['required', 'string', 'max:50'],
@@ -138,15 +165,30 @@ class SurveyKelompokTaniController extends Controller
'nama_gapoktan' => ['nullable', 'string', 'max:255'],
'komoditas_id' => ['nullable', 'array'],
'komoditas_id.*' => ['exists:master_komoditas,id'],
...$this->keanggotaanRules(),
], [
'nama_responden.required' => 'Nama responden wajib diisi.',
'nama_kelompok.required' => 'Nama kelompok wajib diisi.',
'provinsi_id.required' => 'Provinsi wajib diisi.',
'kabupaten_id.required' => 'Kabupaten/Kota wajib diisi.',
'kecamatan_id.required' => 'Kecamatan wajib diisi.',
'provinsi_id.integer' => 'Pilihan Provinsi tidak valid.',
'kabupaten_id.integer' => 'Pilihan Kabupaten/Kota tidak valid.',
'kecamatan_id.integer' => 'Pilihan Kecamatan tidak valid.',
'desa_kelurahan_id.integer' => 'Pilihan Desa/Kelurahan tidak valid.',
'provinsi_id.exists' => 'Provinsi tidak ditemukan.',
'kabupaten_id.exists' => 'Kabupaten/Kota tidak ditemukan.',
'kecamatan_id.exists' => 'Kecamatan tidak ditemukan.',
'desa_kelurahan_id.required' => 'Desa/Kelurahan wajib dipilih.',
'desa_kelurahan_id.exists' => 'Desa/Kelurahan tidak ditemukan.',
'jenis_kelompok.required' => 'Jenis kelompok wajib dipilih.',
'jenis_kelompok_lainnya.required_if' => 'Sebutkan jenis kelompok lainnya.',
'jenis_kelompok.required' => 'Terdaftar/Dikenal Sebagai wajib diisi.',
'jenis_kelompok_lainnya.required_if' => 'Sebutkan Jenis Kelompok wajib diisi.',
'tahun_berdiri.digits' => 'Tahun berdiri harus 4 digit.',
]);
unset($validated['provinsi_id'], $validated['kabupaten_id'], $validated['kecamatan_id']);
return $validated;
}
private function updateIdentitas(Request $request, KelompokTani $kelompokTani)
@@ -163,12 +205,12 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil diperbarui.');
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
}
private function updateRingkasanPengurus(Request $request, KelompokTani $kelompokTani)
private function keanggotaanRules(): array
{
$validated = $request->validate([
return [
'jumlah_pengurus' => ['nullable', 'integer', 'min:0'],
'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'],
@@ -179,18 +221,24 @@ class SurveyKelompokTaniController extends Controller
'jumlah_anggota_perempuan' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_pemuda_laki_laki' => ['nullable', 'integer', 'min:0'],
'jumlah_anggota_pemuda_perempuan' => ['nullable', 'integer', 'min:0'],
]);
];
}
private function updateRingkasanPengurus(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate($this->keanggotaanRules());
$kelompokTani->update([
...$validated,
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'ringkasan', 'Ringkasan pengurus & anggota berhasil disimpan.');
return $this->redirectToSection($this->editUrl($kelompokTani), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
}
private function updateKelembagaan(Request $request, KelompokTani $kelompokTani)
{
$hasSk = in_array($request->input('status_legalitas'), KelompokTani::STATUS_LEGALITAS_DENGAN_SK, true);
$validated = $request->validate([
// Cakupan Wilayah Kelompok
'cakupan_wilayah' => ['nullable', 'string', 'max:50'],
@@ -198,17 +246,9 @@ class SurveyKelompokTaniController extends Controller
// Status Legalitas Kelompok
'status_legalitas' => ['nullable', 'string', 'max:50'],
'nomor_sk' => ['nullable', 'string', 'max:100'],
'tanggal_tahun_penerbitan_sk' => ['nullable', 'string', 'max:30'],
'nomor_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:100'],
'tanggal_tahun_penerbitan_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:30'],
// Kualitas Kepemimpinan
'tingkat_keaktifan_ketua' => ['nullable', 'string', 'max:30'],
'status_pembagian_peran' => ['nullable', 'string', 'max:30'],
'status_dokumentasi_rapat' => ['nullable', 'string', 'max:30'],
// Frekuensi Pertemuan Rutin
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
]);
$kelompokTani->update([
@@ -216,7 +256,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data kelembagaan berhasil disimpan.');
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data Kelembagaan berhasil disimpan.');
}
private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array
@@ -233,6 +273,10 @@ class SurveyKelompokTaniController extends Controller
private function updateAktivitasFungsi(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate([
// Frekuensi Pertemuan Rutin
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
// Kegiatan Konkret yang Sudah Berjalan
'kegiatan_terlaksana' => ['nullable', 'array'],
'kegiatan_terlaksana.*' => ['string', 'max:255'],
@@ -270,7 +314,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data aktivitas & fungsi kelompok berhasil disimpan.');
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data Operasional berhasil disimpan.', 'aset-baku');
}
private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani)
@@ -287,22 +331,28 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'kas', 'Data kas/simpan-pinjam berhasil disimpan.');
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'kas', 'Data Operasional berhasil disimpan.', 'status-bantuan');
}
private function updateSosialKonflik(Request $request, KelompokTani $kelompokTani)
{
$pernahKonflik = in_array($request->input('konflik_kelompok'), [
'Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai',
], true);
$validated = $request->validate([
// Keterlibatan Perempuan
'partisipasi_perempuan' => ['nullable', 'string', 'max:255'],
// Riwayat Konflik Internal Kelompok
'konflik_kelompok' => ['nullable', 'string', 'max:255'],
'topik_konflik' => ['nullable', 'string', 'max:255'],
'topik_konflik' => [Rule::requiredIf($pernahKonflik), 'nullable', 'string', 'max:255'],
'penyelesaian_konflik' => ['nullable', 'string'],
], [
'topik_konflik.required' => 'Jika Pernah, Soal Apa? wajib diisi jika pernah terjadi konflik internal.',
]);
if (!in_array($validated['konflik_kelompok'] ?? null, ['Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai'])) {
if (!$pernahKonflik) {
$validated['topik_konflik'] = null;
$validated['penyelesaian_konflik'] = null;
}
@@ -312,35 +362,7 @@ class SurveyKelompokTaniController extends Controller
'updated_by' => auth()->id(),
]);
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data sosial & konflik berhasil disimpan.');
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data Sosial berhasil disimpan.');
}
private function updateStatusBantuan(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate([
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
]);
$kelompokTani->update([
...$validated,
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'status-bantuan', 'Status akses bantuan berhasil disimpan.');
}
private function updatePenilaian(Request $request, KelompokTani $kelompokTani)
{
$validated = $request->validate([
'potensi_kelompok' => ['nullable', 'string', 'max:50'],
'alasan_penilaian_potensi' => ['nullable', 'string'],
]);
$kelompokTani->update([
...$validated,
'updated_by' => auth()->id(),
]);
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'penilaian', 'Penilaian potensi kelompok berhasil disimpan.');
}
}
@@ -15,6 +15,16 @@ class SurveyProduktivitasController extends Controller
{
$petani = Petani::findOrFail($petaniId);
// Baris yang benar-benar kosong dilewati (sama seperti Pendidikan & Pelatihan)
$request->merge([
'produktivitas_komoditas' => collect($request->input('produktivitas_komoditas', []))
->filter(fn ($row) => filled($row['komoditas_id'] ?? null)
|| filled($row['hasil_panen_3_musim'] ?? null)
|| filled($row['satuan'] ?? null))
->values()
->all(),
]);
$validator = Validator::make($request->all(), [
'produktivitas_komoditas' => ['required', 'array', 'min:1'],
'produktivitas_komoditas.*.komoditas_id' => ['required', 'exists:master_komoditas,id'],
@@ -0,0 +1,135 @@
<?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();
$petani->catatPerubahanStatus(StatusDataPetani::LENGKAP, 'Diverifikasi');
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.");
}
$dari = $petani->status;
$petani->status = StatusDataPetani::DRAFT;
$petani->catatan = trim($data['catatan']);
$petani->updated_by = auth()->id();
$petani->save();
$petani->catatPerubahanStatus($dari, 'Dikembalikan ke draf oleh verifikator', ['catatan' => $petani->catatan]);
return $kembali->with('verifikasi_success', "Data petani {$petani->nama} dikembalikan ke Draf dengan catatan untuk enumerator.");
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Middleware;
use App\Enums\Role;
use App\Enums\StatusDataPetani;
use App\Models\Pertanian\Petani;
use Closure;
use Illuminate\Http\Request;
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
{
private const ROLE_DIIZINKAN = [
Role::SUPER_ADMIN,
Role::ADMIN,
Role::KOORDINATOR,
Role::ENUMERATOR,
];
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
abort_unless($user && in_array($user->role, self::ROLE_DIIZINKAN, true), 403);
$petani = $request->route('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,
'Petani ini bukan bagian dari komoditas yang Anda koordinasikan.'
);
}
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);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Services\Konfigurasi\BufferLogAktivitas;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SimpanLogAktivitas
{
public function __construct(private BufferLogAktivitas $buffer) {}
public function handle(Request $request, Closure $next): Response
{
$this->buffer->mulai();
return $next($request);
}
// Simpan log setelah respons terkirim; request gagal (5xx) tidak dicatat
public function terminate(Request $request, Response $response): void
{
$response->getStatusCode() >= 500 ? $this->buffer->buang() : $this->buffer->simpan();
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace App\Http\Requests\Bumdes;
use App\Enums\Bumdes\AsetProduktifBumdes;
use App\Enums\Bumdes\InfrastrukturPascapanenBumdes;
use App\Enums\Bumdes\JabatanPengurusIntiBumdes;
use App\Enums\Bumdes\KondisiInfrastrukturBumdes;
use App\Enums\Bumdes\StatusAdArtBumdes;
use App\Enums\Bumdes\StatusBadanHukumBumdes;
use App\Enums\Bumdes\StatusKeaktifanBumdes;
use App\Enums\Bumdes\StatusKepemilikanAsetBumdes;
use App\Enums\Bumdes\StatusUnitKomoditasBumdes;
use App\Enums\Bumdes\UnitUsahaBumdes;
use App\Enums\Role;
use App\Models\Bumdes\KelembagaanAktorBumdes;
use App\Models\Bumdes\PengurusBumdes;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class BumdesRequest extends FormRequest
{
public function authorize(): bool
{
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
return false;
}
$pengurus = $this->route('pengurusBumdes');
if ($pengurus instanceof PengurusBumdes && $this->user()->role === Role::ENUMERATOR) {
abort_unless((int) $pengurus->created_by === (int) $this->user()->id, 404);
}
if ($pengurus instanceof PengurusBumdes && $this->routeIs('bumdes.operasional.*', 'bumdes.kelembagaan-aktor.*', 'bumdes.rantai-nilai.*')) {
abort_unless($pengurus->profilLengkap(), 403);
}
return true;
}
public function rules(): array
{
return [];
}
protected function sectionRules(string $section): array
{
$maxYear = now()->year + 1;
$rules = [
'status_badan_hukum' => ['nullable', Rule::enum(StatusBadanHukumBumdes::class)],
'nomor_dasar_hukum' => [Rule::excludeIf($this->input('status_badan_hukum') !== 'sudah_berbadan_hukum'), 'required_if:status_badan_hukum,sudah_berbadan_hukum', 'string', 'max:255'],
'tahun_dasar_hukum' => [Rule::excludeIf($this->input('status_badan_hukum') !== 'sudah_berbadan_hukum'), 'required_if:status_badan_hukum,sudah_berbadan_hukum', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear],
'status_keaktifan' => ['nullable', Rule::enum(StatusKeaktifanBumdes::class)],
'alasan_tidak_aktif' => ['nullable', 'string'],
'unit_usaha' => ['nullable', 'array'],
'unit_usaha.*' => ['string', Rule::enum(UnitUsahaBumdes::class)],
'unit_usaha_jasa_lainnya' => ['nullable', 'string', 'max:255'],
'status_unit_komoditas' => ['nullable', Rule::enum(StatusUnitKomoditasBumdes::class)],
'nama_unit_komoditas' => [Rule::excludeIf($this->input('status_unit_komoditas') !== 'ya'), 'required_if:status_unit_komoditas,ya', 'string', 'max:255'],
'status_ad_art' => ['nullable', Rule::enum(StatusAdArtBumdes::class)],
'tahun_ad_art' => [Rule::excludeIf($this->input('status_ad_art') !== 'ada_lengkap'), 'required_if:status_ad_art,ada_lengkap', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear],
'pengurus_inti' => ['nullable', 'array'],
'pengurus_inti.*' => ['array'],
'pengurus_inti.*.jabatan' => ['nullable', Rule::enum(JabatanPengurusIntiBumdes::class)],
'pengurus_inti.*.nama' => ['nullable', 'string', 'max:255'],
'pengurus_inti.*.aktif' => ['nullable', 'boolean'],
'memiliki_staf' => ['nullable', 'boolean'],
'jumlah_staf' => ['nullable', 'integer', 'min:0'],
'unit_usaha_staf' => ['nullable', 'string'],
'modal_awal' => ['nullable', 'integer', 'min:0'],
'tahun_modal_awal' => ['nullable', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear],
'pernah_penambahan_modal' => ['nullable', 'boolean'],
'penambahan_modal' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'array'],
'penambahan_modal.*' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'array'],
'penambahan_modal.*.tahun' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'integer', 'digits:4', 'min:1900', 'max:'.$maxYear],
'penambahan_modal.*.jumlah' => [Rule::excludeIf(! $this->boolean('pernah_penambahan_modal')), 'nullable', 'integer', 'min:0'],
'aset_produktif' => ['nullable', 'array'],
'aset_produktif.*' => ['array'],
'aset_produktif.*.aset' => ['nullable', Rule::enum(AsetProduktifBumdes::class)],
'aset_produktif.*.dipilih' => ['nullable', 'boolean'],
'aset_produktif.*.jumlah' => ['nullable', 'integer', 'min:0'],
'aset_produktif.*.kondisi' => ['nullable', 'string', 'max:255'],
'aset_produktif_lainnya' => ['nullable', 'string', 'max:255'],
'status_kepemilikan_aset' => ['nullable', Rule::enum(StatusKepemilikanAsetBumdes::class)],
'infrastruktur_pascapanen' => ['nullable', 'array'],
'infrastruktur_pascapanen.*' => ['array'],
'infrastruktur_pascapanen.*.infrastruktur' => ['nullable', Rule::enum(InfrastrukturPascapanenBumdes::class)],
'infrastruktur_pascapanen.*.dipilih' => ['nullable', 'boolean'],
'infrastruktur_pascapanen.*.jumlah' => ['nullable', 'integer', 'min:0'],
'infrastruktur_pascapanen.*.kondisi' => ['nullable', Rule::enum(KondisiInfrastrukturBumdes::class)],
'infrastruktur_pascapanen_lainnya' => ['nullable', 'string', 'max:255'],
];
$fields = KelembagaanAktorBumdes::sectionFields($section);
$rules = array_filter($rules, fn ($field) => in_array(Str::before($field, '.'), $fields, true), ARRAY_FILTER_USE_KEY);
return $rules;
}
public function messages(): array
{
return [
'nomor_dasar_hukum.required_if' => 'Nomor Peraturan Desa/SK wajib diisi.',
'tahun_dasar_hukum.required_if' => 'Tahun Ditetapkan wajib diisi.',
'nama_unit_komoditas.required_if' => 'Nama Unit Usaha Komoditas wajib diisi.',
'tahun_ad_art.required_if' => 'Tahun Terakhir Diperbarui wajib diisi.',
'string' => ':attribute harus berupa teks.',
'required' => ':attribute wajib diisi.',
'exists' => ':attribute tidak ditemukan.',
'integer' => ':attribute harus berupa angka.',
'digits' => ':attribute harus terdiri dari :digits digit.',
'min' => ':attribute tidak boleh kurang dari :min.',
'max' => ':attribute tidak valid.',
'enum' => 'Pilihan :attribute tidak valid.',
'in' => ':attribute tidak valid.',
'array' => ':attribute harus berupa daftar data.',
'boolean' => ':attribute harus dipilih.',
];
}
public function attributes(): array
{
return [
'search' => 'Pencarian',
'kabupaten_kota_id' => 'Kabupaten/Kota', 'kabupaten_id' => 'Kabupaten/Kota',
'provinsi_id' => 'Provinsi', 'kecamatan_id' => 'Kecamatan', 'desa_kelurahan_id' => 'Desa/Kelurahan',
'jenis_kelamin' => 'Jenis kelamin', 'per_page' => 'Jumlah data per halaman',
'nama' => 'Nama', 'tempat_lahir' => 'Tempat lahir', 'tanggal_lahir' => 'Tanggal lahir',
'pendidikan_terakhir' => 'Pendidikan terakhir', 'alamat' => 'Alamat', 'nomor_telepon' => 'Nomor telepon',
'jabatan' => 'Jabatan/peran', 'jabatan_lainnya' => 'Jabatan lainnya', 'lama_menjabat_tahun' => 'Lama menjabat',
'pengurus_inti' => 'Daftar Pengurus', 'pengurus_inti.*.jabatan' => 'Jabatan pengurus',
'pengurus_inti.*.nama' => 'Nama pengurus', 'pengurus_inti.*.aktif' => 'Keaktifan pengurus',
'status_badan_hukum' => 'Status badan hukum',
'nomor_dasar_hukum' => 'Nomor Peraturan Desa/SK',
'tahun_dasar_hukum' => 'Tahun Ditetapkan',
'nama_unit_komoditas' => 'Nama Unit Usaha Komoditas',
'tahun_ad_art' => 'Tahun Terakhir Diperbarui',
'status_keaktifan' => 'Status keaktifan',
'unit_usaha' => 'Unit usaha',
'status_unit_komoditas' => 'Unit usaha komoditas',
'status_ad_art' => 'Dokumen AD/ART',
'memiliki_staf' => 'Status staf/karyawan',
'jumlah_staf' => 'Jumlah staf',
'modal_awal' => 'Modal awal',
'tahun_modal_awal' => 'Tahun modal awal',
'pernah_penambahan_modal' => 'Status penambahan modal',
'penambahan_modal' => 'Rincian penambahan modal',
'penambahan_modal.*.tahun' => 'Tahun penambahan modal pada baris :position',
'penambahan_modal.*.jumlah' => 'Jumlah penambahan modal pada baris :position',
'status_kepemilikan_aset' => 'Status kepemilikan aset',
'alasan_tidak_aktif' => 'Alasan tidak aktif',
'unit_usaha.*' => 'Pilihan unit usaha',
'unit_usaha_jasa_lainnya' => 'Jasa lainnya',
'unit_usaha_staf' => 'Unit usaha staf',
'pengurus_inti.*' => 'Data pengurus pada baris :position',
'penambahan_modal.*' => 'Penambahan modal pada baris :position',
'aset_produktif' => 'Aset produktif',
'aset_produktif.*' => 'Data aset produktif pada baris :position',
'aset_produktif.*.aset' => 'Jenis aset produktif',
'aset_produktif.*.dipilih' => 'Pilihan aset produktif',
'aset_produktif.*.jumlah' => 'Jumlah aset produktif',
'aset_produktif.*.kondisi' => 'Kondisi aset produktif',
'aset_produktif_lainnya' => 'Aset produktif lainnya',
'infrastruktur_pascapanen' => 'Infrastruktur pascapanen',
'infrastruktur_pascapanen.*' => 'Data infrastruktur pada baris :position',
'infrastruktur_pascapanen.*.infrastruktur' => 'Jenis infrastruktur',
'infrastruktur_pascapanen.*.dipilih' => 'Pilihan infrastruktur',
'infrastruktur_pascapanen.*.jumlah' => 'Jumlah infrastruktur',
'infrastruktur_pascapanen.*.kondisi' => 'Kondisi infrastruktur',
'infrastruktur_pascapanen_lainnya' => 'Infrastruktur lainnya',
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Bumdes;
use App\Enums\Bumdes\StatusKeaktifanBumdes;
use Illuminate\Validation\Rule;
class IndexPengurusBumdesRequest extends BumdesRequest
{
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'kabupaten_kota_id' => ['nullable', 'integer', 'exists:master_kabupaten_kota,id'],
'kecamatan_id' => ['nullable', 'integer', 'exists:master_kecamatan,id'],
'desa_kelurahan_id' => ['nullable', 'integer', 'exists:master_desa_kelurahan,id'],
'status_keaktifan' => ['nullable', Rule::enum(StatusKeaktifanBumdes::class)],
'sort_by' => ['nullable', 'string'],
'per_page' => ['nullable', 'integer', 'in:10,25'],
];
}
public function messages(): array
{
return [...parent::messages(), 'exists' => ':attribute tidak ditemukan.', 'string' => ':attribute harus berupa teks.', 'max' => ':attribute terlalu panjang.', 'in' => 'Pilihan :attribute tidak valid.'];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Http\Requests\Bumdes;
class StoreKelembagaanAktorBumdesRequest extends BumdesRequest
{
public function rules(): array
{
return $this->sectionRules('kelembagaan');
}
}
@@ -0,0 +1,252 @@
<?php
namespace App\Http\Requests\Bumdes;
use App\Enums\Bumdes\CakupanLayananBumdes;
use App\Enums\Bumdes\DukunganDesaBumdes;
use App\Enums\Bumdes\FormatPembukuanBumdes;
use App\Enums\Bumdes\HambatanUsahaBumdes;
use App\Enums\Bumdes\KemitraanEksternalBumdes;
use App\Enums\Bumdes\KeterlibatanKoordinasiBumdes;
use App\Enums\Bumdes\KondisiAsetProduktifBumdes;
use App\Enums\Bumdes\KondisiInfrastrukturBumdes;
use App\Enums\Bumdes\KondisiKeuanganBumdes;
use App\Enums\Bumdes\KontribusiPadesBumdes;
use App\Enums\Bumdes\LayananPetaniBumdes;
use App\Enums\Bumdes\PencatatanPetaniBumdes;
use App\Enums\Bumdes\PengalamanOfftakerBumdes;
use App\Enums\Bumdes\PeranRantaiNilaiBumdes;
use App\Enums\Bumdes\PertanggungjawabanMusdesBumdes;
use App\Enums\Bumdes\SifatAuditBumdes;
use App\Enums\Bumdes\StatusKepemilikanAsetBumdes;
use App\Enums\Bumdes\StatusPembukuanBumdes;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StoreOperasionalBumdesRequest extends BumdesRequest
{
public function rules(): array
{
$rules = [...$this->sectionRules('operasional'), ...$this->pengelolaanRules()];
$dukungan = $this->input('dukungan_desa', []);
$kemitraan = $this->input('kemitraan_eksternal', []);
$dukunganLainnya = is_array($dukungan) && in_array(DukunganDesaBumdes::LAINNYA->value, $dukungan, true);
$kemitraanLainnya = is_array($kemitraan) && in_array(KemitraanEksternalBumdes::LAINNYA->value, $kemitraan, true);
$rules = [...$rules,
'ada_alokasi_desa' => ['nullable', 'boolean'],
'jumlah_alokasi_desa' => [Rule::excludeIf(! $this->boolean('ada_alokasi_desa')), 'required_if:ada_alokasi_desa,1', 'integer', 'min:0', 'max:9999999999999999'],
'dukungan_desa' => ['nullable', 'array'],
'dukungan_desa.*' => ['string', 'distinct', Rule::enum(DukunganDesaBumdes::class)],
'dukungan_desa_lainnya' => [Rule::excludeIf(! $dukunganLainnya), Rule::requiredIf($dukunganLainnya), 'string', 'max:255'],
'keterlibatan_koordinasi_desa' => ['nullable', Rule::enum(KeterlibatanKoordinasiBumdes::class)],
'kemitraan_eksternal' => ['nullable', 'array'],
'kemitraan_eksternal.*' => ['string', 'distinct', Rule::enum(KemitraanEksternalBumdes::class)],
'kemitraan_eksternal_lainnya' => [Rule::excludeIf(! $kemitraanLainnya), Rule::requiredIf($kemitraanLainnya), 'string', 'max:255'],
];
$saved = collect($this->route('pengurusBumdes')?->kelembagaanAktor?->aset_produktif ?? [])->keyBy('aset');
$rows = $this->input('aset_produktif', []);
$hasOtherAsset = false;
foreach (is_array($rows) ? $rows : [] as $index => $row) {
if (! is_array($row)) {
continue;
}
$exists = in_array($row['dipilih'] ?? null, [true, 1, '1'], true);
$assetKey = is_string($row['aset'] ?? null) ? $row['aset'] : '';
$hasOtherAsset = $hasOtherAsset || ($exists && ($row['aset'] ?? null) === 'lainnya');
$conditions = array_keys(KondisiAsetProduktifBumdes::options());
// Kondisi teks lama tetap dapat ditampilkan dan disimpan untuk aset yang sama.
$legacy = KondisiAsetProduktifBumdes::normalizeLegacy(data_get($saved->get($assetKey), 'kondisi'));
if ($legacy !== null) {
$conditions[] = $legacy;
}
$prefix = 'aset_produktif.'.$index.'.';
$rules[$prefix.'status_kepemilikan_aset'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::enum(StatusKepemilikanAsetBumdes::class)];
$rules[$prefix.'kondisi'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::in($conditions)];
$rules[$prefix.'jumlah'] = [Rule::excludeIf(! $exists), 'nullable', 'integer', 'min:0'];
}
$rules['aset_produktif_lainnya'] = [Rule::excludeIf(! $hasOtherAsset), Rule::requiredIf($hasOtherAsset), 'string', 'max:255'];
$rows = $this->input('infrastruktur_pascapanen', []);
$hasOtherInfrastructure = false;
foreach (is_array($rows) ? $rows : [] as $index => $row) {
if (! is_array($row)) {
continue;
}
$exists = in_array($row['dipilih'] ?? null, [true, 1, '1'], true);
$hasOtherInfrastructure = $hasOtherInfrastructure || ($exists && ($row['infrastruktur'] ?? null) === 'lainnya');
$prefix = 'infrastruktur_pascapanen.'.$index.'.';
$rules[$prefix.'kondisi'] = [Rule::excludeIf(! $exists), Rule::requiredIf($exists), Rule::enum(KondisiInfrastrukturBumdes::class)];
$rules[$prefix.'jumlah'] = [Rule::excludeIf(! $exists), 'nullable', 'integer', 'min:0'];
}
$rules['infrastruktur_pascapanen_lainnya'] = [Rule::excludeIf(! $hasOtherInfrastructure), Rule::requiredIf($hasOtherInfrastructure), 'string', 'max:255'];
return $rules;
}
private function pengelolaanRules(): array
{
$hasAudit = $this->boolean('pernah_audit');
$annualReport = $this->input('pertanggungjawaban_musdes') === PertanggungjawabanMusdesBumdes::TAHUNAN->value;
$hasPades = in_array($this->input('kontribusi_pades'), [KontribusiPadesBumdes::RUTIN->value, KontribusiPadesBumdes::TIDAK_RUTIN->value], true);
$otherVillage = $this->input('cakupan_layanan') === CakupanLayananBumdes::DESA_LAIN->value;
$activePartnership = $this->input('pengalaman_offtaker') === PengalamanOfftakerBumdes::BERJALAN->value;
$stoppedPartnership = $this->input('pengalaman_offtaker') === PengalamanOfftakerBumdes::BERHENTI->value;
$hasOther = fn (string $field, string $value) => is_array($selected = $this->input($field)) && in_array($value, $selected, true);
$otherService = $hasOther('layanan_petani', LayananPetaniBumdes::LAINNYA->value);
$otherObstacle = $hasOther('hambatan_usaha', HambatanUsahaBumdes::LAINNYA->value);
$year = ['integer', 'digits:4', 'min:1900', 'max:'.now()->year];
return [
'status_pembukuan' => ['nullable', Rule::enum(StatusPembukuanBumdes::class)],
'format_pembukuan' => ['nullable', Rule::enum(FormatPembukuanBumdes::class)],
'pernah_audit' => ['nullable', 'boolean'],
'sifat_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), 'array', 'min:1'],
'sifat_audit.*' => [Rule::excludeIf(! $hasAudit), 'string', 'distinct', Rule::enum(SifatAuditBumdes::class)],
'pelaksana_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), 'string', 'max:255'],
'tahun_audit' => [Rule::excludeIf(! $hasAudit), Rule::requiredIf($hasAudit), ...$year],
'pertanggungjawaban_musdes' => ['nullable', Rule::enum(PertanggungjawabanMusdesBumdes::class)],
'tahun_laporan_musdes' => [Rule::excludeIf(! $annualReport), Rule::requiredIf($annualReport), ...$year],
'kondisi_keuangan' => ['nullable', Rule::enum(KondisiKeuanganBumdes::class)],
'kontribusi_pades' => ['nullable', Rule::enum(KontribusiPadesBumdes::class)],
'nominal_kontribusi_pades' => [Rule::excludeIf(! $hasPades), Rule::requiredIf($hasPades), 'integer', 'min:0', 'max:9999999999999999'],
'cakupan_layanan' => ['nullable', Rule::enum(CakupanLayananBumdes::class)],
'desa_lain_dilayani' => [Rule::excludeIf(! $otherVillage), Rule::requiredIf($otherVillage), 'string', 'max:2000'],
'jumlah_petani_desa_sendiri' => ['nullable', 'integer', 'min:0', 'max:2147483647'],
'jumlah_petani_desa_lain' => [Rule::excludeIf(! $otherVillage), Rule::requiredIf($otherVillage), 'integer', 'min:0', 'max:2147483647'],
'pencatatan_petani' => ['nullable', Rule::enum(PencatatanPetaniBumdes::class)],
'layanan_petani' => ['nullable', 'array'],
'layanan_petani.*' => ['string', 'distinct', Rule::enum(LayananPetaniBumdes::class)],
'layanan_petani_lainnya' => [Rule::excludeIf(! $otherService), Rule::requiredIf($otherService), 'string', 'max:255'],
'peran_rantai_nilai' => ['nullable', Rule::enum(PeranRantaiNilaiBumdes::class)],
'pengalaman_offtaker' => ['nullable', Rule::enum(PengalamanOfftakerBumdes::class)],
'nama_offtaker' => [Rule::excludeIf(! $activePartnership), Rule::requiredIf($activePartnership), 'string', 'max:255'],
'tahun_mulai_offtaker' => [Rule::excludeIf(! $activePartnership), Rule::requiredIf($activePartnership), ...$year],
'alasan_berhenti_offtaker' => [Rule::excludeIf(! $stoppedPartnership), Rule::requiredIf($stoppedPartnership), 'string', 'max:2000'],
'hambatan_usaha' => ['nullable', 'array'],
'hambatan_usaha.*' => ['string', 'distinct', Rule::enum(HambatanUsahaBumdes::class)],
'hambatan_usaha_lainnya' => [Rule::excludeIf(! $otherObstacle), Rule::requiredIf($otherObstacle), 'string', 'max:255'],
];
}
protected function prepareForValidation(): void
{
foreach (['jumlah_alokasi_desa', 'nominal_kontribusi_pades'] as $field) {
$amount = $this->input($field);
if (is_string($amount) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $amount)) {
$this->merge([$field => str_replace('.', '', $amount)]);
}
}
$normalize = static fn ($value) => is_scalar($value) ? preg_replace('/\D/', '', (string) $value) : $value;
$data = [];
if ($this->has('modal_awal')) {
$data['modal_awal'] = $normalize($this->input('modal_awal'));
}
if (is_array($this->input('penambahan_modal'))) {
$data['penambahan_modal'] = array_map(function ($row) use ($normalize) {
if (is_array($row) && array_key_exists('jumlah', $row)) {
$row['jumlah'] = $normalize($row['jumlah']);
}
return $row;
}, $this->input('penambahan_modal'));
}
if (is_array($this->input('aset_produktif'))) {
$legacyOwnership = $this->input('status_kepemilikan_aset')
?? $this->route('pengurusBumdes')?->kelembagaanAktor?->status_kepemilikan_aset?->value;
$data['aset_produktif'] = array_map(function ($row) use ($legacyOwnership) {
if (! is_array($row)) {
return $row;
}
if (! array_key_exists('status_kepemilikan_aset', $row)) {
$row['status_kepemilikan_aset'] = $legacyOwnership;
}
if (is_scalar($row['kondisi'] ?? null)) {
$row['kondisi'] = KondisiAsetProduktifBumdes::normalizeLegacy($row['kondisi']);
}
return $row;
}, $this->input('aset_produktif'));
}
$this->merge($data);
}
public function after(): array
{
return [function (Validator $validator) {
foreach (['dukungan_desa' => DukunganDesaBumdes::TIDAK_ADA->value, 'kemitraan_eksternal' => KemitraanEksternalBumdes::BELUM_ADA->value, 'layanan_petani' => LayananPetaniBumdes::BELUM_ADA->value] as $field => $none) {
$selected = $this->input($field, []);
if (is_array($selected) && in_array($none, $selected, true) && count($selected) > 1) {
$validator->errors()->add($field, match ($field) {
'dukungan_desa' => 'Tidak ada dukungan khusus tidak boleh dipilih bersama bentuk dukungan lainnya.',
'kemitraan_eksternal' => 'Belum ada kemitraan eksternal tidak boleh dipilih bersama jenis kemitraan lainnya.',
'layanan_petani' => 'Belum ada layanan tidak boleh dipilih bersama jenis layanan lainnya.',
});
}
}
}];
}
public function messages(): array
{
return [...parent::messages(),
'jumlah_alokasi_desa.required_if' => 'Besaran alokasi APBDes/Dana Desa wajib diisi jika ada alokasi tahun berjalan.',
'jumlah_alokasi_desa.max' => 'Besaran alokasi APBDes/Dana Desa tidak boleh melebihi Rp 9.999.999.999.999.999.',
'dukungan_desa_lainnya.required' => 'Bentuk dukungan lainnya wajib diisi jika memilih Lainnya.',
'kemitraan_eksternal_lainnya.required' => 'Jenis kemitraan lainnya wajib diisi jika memilih Lainnya.',
'nominal_kontribusi_pades.max' => 'Nominal kontribusi PADes tidak boleh melebihi Rp 9.999.999.999.999.999.',
'sifat_audit.min' => 'Pilih minimal satu sifat audit/pemeriksaan.',
'distinct' => ':attribute tidak boleh dipilih lebih dari satu kali.',
'aset_produktif.*.status_kepemilikan_aset.required' => 'Status Kepemilikan wajib diisi jika aset dipilih Ada.',
'aset_produktif.*.kondisi.required' => 'Kondisi wajib diisi jika aset dipilih Ada.',
'aset_produktif_lainnya.required' => 'Nama aset lainnya wajib diisi jika dipilih Ada.',
'infrastruktur_pascapanen.*.kondisi.required' => 'Kondisi wajib diisi jika infrastruktur dipilih Ada.',
'infrastruktur_pascapanen_lainnya.required' => 'Nama infrastruktur lainnya wajib diisi jika dipilih Ada.',
];
}
public function attributes(): array
{
return [...parent::attributes(),
'status_pembukuan' => 'Sistem pembukuan dan laporan keuangan',
'format_pembukuan' => 'Format pembukuan',
'pernah_audit' => 'Status audit/pemeriksaan',
'sifat_audit' => 'Sifat audit/pemeriksaan',
'pelaksana_audit' => 'Pelaksana audit/pemeriksaan',
'tahun_audit' => 'Tahun terakhir audit/pemeriksaan',
'pertanggungjawaban_musdes' => 'Pertanggungjawaban ke musyawarah desa',
'tahun_laporan_musdes' => 'Tahun terakhir laporan ke musyawarah desa',
'kondisi_keuangan' => 'Kondisi keuangan',
'kontribusi_pades' => 'Kontribusi ke PADes',
'nominal_kontribusi_pades' => 'Nominal kontribusi ke PADes',
'cakupan_layanan' => 'Cakupan wilayah layanan',
'desa_lain_dilayani' => 'Desa lain yang dilayani',
'jumlah_petani_desa_sendiri' => 'Jumlah petani dari desa sendiri',
'jumlah_petani_desa_lain' => 'Jumlah petani dari desa lain',
'pencatatan_petani' => 'Pencatatan petani yang dilayani',
'layanan_petani' => 'Layanan bagi petani',
'layanan_petani_lainnya' => 'Layanan lainnya',
'peran_rantai_nilai' => 'Peran dalam rantai nilai',
'pengalaman_offtaker' => 'Pengalaman kemitraan dengan offtaker',
'nama_offtaker' => 'Nama offtaker',
'tahun_mulai_offtaker' => 'Tahun mulai kemitraan',
'alasan_berhenti_offtaker' => 'Alasan kemitraan berhenti',
'hambatan_usaha' => 'Hambatan usaha',
'hambatan_usaha_lainnya' => 'Hambatan lainnya',
'sifat_audit.*' => 'Pilihan sifat audit/pemeriksaan',
'layanan_petani.*' => 'Pilihan layanan bagi petani',
'hambatan_usaha.*' => 'Pilihan hambatan usaha',
'ada_alokasi_desa' => 'Status alokasi APBDes/Dana Desa',
'jumlah_alokasi_desa' => 'Besaran alokasi APBDes/Dana Desa',
'dukungan_desa' => 'Bentuk dukungan pemerintah desa',
'dukungan_desa.*' => 'Pilihan dukungan pemerintah desa',
'dukungan_desa_lainnya' => 'Bentuk dukungan lainnya',
'keterlibatan_koordinasi_desa' => 'Keterlibatan dalam rapat/koordinasi desa',
'kemitraan_eksternal' => 'Kemitraan eksternal',
'kemitraan_eksternal.*' => 'Pilihan kemitraan eksternal',
'kemitraan_eksternal_lainnya' => 'Jenis kemitraan lainnya',
'aset_produktif.*.status_kepemilikan_aset' => 'Status Kepemilikan aset',
];
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Requests\Bumdes;
use App\Enums\Bumdes\JabatanBumdes;
use App\Enums\Pendidikan;
use Illuminate\Validation\Rule;
class StorePengurusBumdesRequest extends BumdesRequest
{
public function rules(): array
{
return [
'nama' => ['required', 'string', 'max:255'],
'jenis_kelamin' => ['required', 'boolean'],
'tempat_lahir' => ['required', 'string', 'max:255'],
'tanggal_lahir' => ['required', 'date_format:d-m-Y'],
'pendidikan_terakhir' => ['required', Rule::enum(Pendidikan::class)],
'alamat' => ['required', 'string', 'max:500'],
'provinsi_id' => ['bail', 'required', 'integer', 'exists:master_provinsi,id'],
'kabupaten_id' => ['bail', 'required', 'integer', 'exists:master_kabupaten_kota,id'],
'kecamatan_id' => ['bail', 'required', 'integer', 'exists:master_kecamatan,id'],
'desa_kelurahan_id' => ['bail', 'required', 'integer', 'exists:master_desa_kelurahan,id'],
'nomor_telepon' => ['nullable', 'digits_between:11,13'],
'jabatan' => ['required', Rule::enum(JabatanBumdes::class)],
'jabatan_lainnya' => ['nullable', 'required_if:jabatan,'.JabatanBumdes::LAINNYA->value, 'string', 'max:255'],
'lama_menjabat_tahun' => ['required', 'integer', 'min:0', 'max:80'],
...$this->sectionRules('profil'),
];
}
public function messages(): array
{
return [
...parent::messages(),
'nama.required' => 'Nama harus diisi.',
'jenis_kelamin.required' => 'Jenis kelamin harus dipilih.',
'tempat_lahir.required' => 'Tempat lahir harus diisi.',
'tanggal_lahir.required' => 'Tanggal lahir harus diisi.',
'tanggal_lahir.date_format' => 'Tanggal lahir harus menggunakan format tanggal yang valid.',
'pendidikan_terakhir.required' => 'Pendidikan terakhir harus dipilih.',
'alamat.required' => 'Alamat harus diisi.',
'provinsi_id.required' => 'Provinsi wajib dipilih.',
'kabupaten_id.required' => 'Kabupaten/Kota wajib dipilih.',
'kecamatan_id.required' => 'Kecamatan wajib dipilih.',
'provinsi_id.integer' => 'Pilihan Provinsi tidak valid.',
'kabupaten_id.integer' => 'Pilihan Kabupaten/Kota tidak valid.',
'kecamatan_id.integer' => 'Pilihan Kecamatan tidak valid.',
'desa_kelurahan_id.integer' => 'Pilihan Desa/Kelurahan tidak valid.',
'provinsi_id.exists' => 'Provinsi tidak ditemukan.',
'kabupaten_id.exists' => 'Kabupaten/Kota tidak ditemukan.',
'kecamatan_id.exists' => 'Kecamatan tidak ditemukan.',
'desa_kelurahan_id.required' => 'Desa kelurahan harus dipilih.',
'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.',
'nomor_telepon.digits_between' => 'Nomor telepon harus berupa angka 11 sampai 13 digit.',
'jabatan.required' => 'Jabatan/peran harus dipilih.',
'jabatan_lainnya.required_if' => 'Jabatan lainnya harus diisi.',
'lama_menjabat_tahun.required' => 'Lama menjabat harus diisi.',
'lama_menjabat_tahun.integer' => 'Lama menjabat harus berupa angka tahun.',
'string' => ':attribute harus berupa teks.',
'max' => ':attribute tidak valid.',
'min' => ':attribute tidak boleh kurang dari :min.',
'enum' => 'Pilihan :attribute tidak valid.',
];
}
}
@@ -0,0 +1,172 @@
<?php
namespace App\Http\Requests\Bumdes;
use App\Enums\Bumdes\BentukProdukBumdes;
use App\Enums\Bumdes\HubunganPemasokBumdes;
use App\Enums\Bumdes\MekanismePembayaranBumdes;
use App\Enums\Bumdes\ModelFermentasiBumdes;
use App\Enums\Bumdes\PenetapanHargaBeliBumdes;
use App\Enums\Bumdes\PeriodePanenBumdes;
use App\Enums\Bumdes\PerjanjianPetaniBumdes;
use App\Enums\Bumdes\SaluranPenjualanBumdes;
use App\Enums\Bumdes\StatusFermentasiBumdes;
use App\Enums\Bumdes\StatusPengeringanBumdes;
use App\Enums\Bumdes\StatusSortasiBumdes;
use App\Enums\Bumdes\SumberPasokanBumdes;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StoreRantaiNilaiBumdesRequest extends BumdesRequest
{
public function rules(): array
{
$trading = $this->boolean('melakukan_jual_beli');
$hasChoice = fn (string $field, string $value) => is_array($selected = $this->input($field)) && in_array($value, $selected, true);
$notSold = $hasChoice('saluran_penjualan', SaluranPenjualanBumdes::BELUM_MENJUAL->value);
$otherSource = $trading && $hasChoice('sumber_pasokan', SumberPasokanBumdes::LAINNYA->value);
$otherChannel = $trading && $hasChoice('saluran_penjualan', SaluranPenjualanBumdes::LAINNYA->value);
$otherRelationship = $trading && $hasChoice('hubungan_petani_pemasok', HubunganPemasokBumdes::LAINNYA->value);
$otherPayment = $trading && $this->input('mekanisme_pembayaran_petani') === MekanismePembayaranBumdes::LAINNYA->value;
$otherPrice = $trading && $this->input('penetapan_harga_beli') === PenetapanHargaBeliBumdes::LAINNYA->value;
$periods = implode(',', array_keys(PeriodePanenBumdes::options()));
$products = implode(',', array_keys(BentukProdukBumdes::options()));
$rules = [...$this->pascapanenRules(),
'melakukan_jual_beli' => ['required', 'boolean'],
'redirect_fragment' => ['nullable', Rule::in(['section-rantai-penjualan', 'section-rantai-hubungan-pemasok', 'section-rantai-harga-beli', 'section-rantai-sortasi', 'section-rantai-pengeringan', 'section-rantai-fermentasi', 'section-rantai-premium-harga'])],
'sumber_pasokan' => [Rule::excludeIf(! $trading), 'nullable', 'array'],
'sumber_pasokan.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(SumberPasokanBumdes::class)],
'sumber_pasokan_lainnya' => [Rule::excludeIf(! $otherSource), Rule::requiredIf($otherSource), 'string', 'max:255'],
'bentuk_produk_dibeli' => [Rule::excludeIf(! $trading), 'nullable', 'array'],
'bentuk_produk_dibeli.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(BentukProdukBumdes::class)],
'volume_pembelian' => [Rule::excludeIf(! $trading), 'nullable', 'array:'.$periods],
'volume_pembelian.*' => [Rule::excludeIf(! $trading), 'nullable', 'numeric', 'decimal:0,3', 'min:0', 'max:999999999999.999'],
'saluran_penjualan' => [Rule::excludeIf(! $trading), 'nullable', 'array'],
'saluran_penjualan.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(SaluranPenjualanBumdes::class)],
'saluran_penjualan_lainnya' => [Rule::excludeIf(! $otherChannel), Rule::requiredIf($otherChannel), 'string', 'max:255'],
'bentuk_produk_dijual' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'array'],
'bentuk_produk_dijual.*' => [Rule::excludeIf(! $trading || $notSold), 'string', 'distinct', Rule::enum(BentukProdukBumdes::class)],
'volume_penjualan' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'array:'.$periods],
'volume_penjualan.*' => [Rule::excludeIf(! $trading || $notSold), 'nullable', 'numeric', 'decimal:0,3', 'min:0', 'max:999999999999.999'],
'hubungan_petani_pemasok' => [Rule::excludeIf(! $trading), 'nullable', 'array'],
'hubungan_petani_pemasok.*' => [Rule::excludeIf(! $trading), 'string', 'distinct', Rule::enum(HubunganPemasokBumdes::class)],
'hubungan_petani_pemasok_lainnya' => [Rule::excludeIf(! $otherRelationship), Rule::requiredIf($otherRelationship), 'string', 'max:255'],
'mekanisme_pembayaran_petani' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(MekanismePembayaranBumdes::class)],
'mekanisme_pembayaran_lainnya' => [Rule::excludeIf(! $otherPayment), Rule::requiredIf($otherPayment), 'string', 'max:255'],
'perjanjian_petani' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(PerjanjianPetaniBumdes::class)],
'harga_beli_petani' => [Rule::excludeIf(! $trading), 'nullable', 'array:'.$products],
'penetapan_harga_beli' => [Rule::excludeIf(! $trading), 'nullable', Rule::enum(PenetapanHargaBeliBumdes::class)],
'penetapan_harga_beli_lainnya' => [Rule::excludeIf(! $otherPrice), Rule::requiredIf($otherPrice), 'string', 'max:255'],
];
foreach (BentukProdukBumdes::cases() as $product) {
$enabled = $trading && $hasChoice('bentuk_produk_dibeli', $product->value);
$prefix = 'harga_beli_petani.'.$product->value;
$rules[$prefix] = [Rule::excludeIf(! $enabled), 'nullable', 'array:'.$periods];
$rules[$prefix.'.*'] = [Rule::excludeIf(! $enabled), 'nullable', 'integer', 'min:0', 'max:9999999999999999'];
}
return $rules;
}
private function pascapanenRules(): array
{
$processing = $this->boolean('melakukan_pengolahan_pascapanen');
$sorting = $processing && in_array($this->input('status_sortasi'), [StatusSortasiBumdes::RUTIN_STANDAR->value, StatusSortasiBumdes::TIDAK_KONSISTEN->value], true);
$drying = $processing && in_array($this->input('status_pengeringan'), [StatusPengeringanBumdes::RUTIN->value, StatusPengeringanBumdes::KADANG->value], true);
$fermenting = $processing && in_array($this->input('status_fermentasi'), [StatusFermentasiBumdes::RUTIN->value, StatusFermentasiBumdes::PERNAH_MENCOBA->value], true);
return [
'melakukan_pengolahan_pascapanen' => ['required', 'boolean'],
'status_sortasi' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusSortasiBumdes::class)],
'kriteria_sortasi' => [Rule::excludeIf(! $sorting), Rule::requiredIf($sorting), 'string', 'max:2000'],
'status_pengeringan' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusPengeringanBumdes::class)],
'cara_tempat_pengeringan' => [Rule::excludeIf(! $drying), Rule::requiredIf($drying), 'string', 'max:2000'],
'status_fermentasi' => [Rule::excludeIf(! $processing), 'nullable', Rule::enum(StatusFermentasiBumdes::class)],
'model_fermentasi' => [Rule::excludeIf(! $fermenting), Rule::requiredIf($fermenting), 'array', 'min:1'],
'model_fermentasi.*' => [Rule::excludeIf(! $fermenting), 'string', 'distinct', Rule::enum(ModelFermentasiBumdes::class)],
'proses_fermentasi' => [Rule::excludeIf(! $fermenting), Rule::requiredIf($fermenting), 'string', 'max:2000'],
'premium_harga_pascapanen' => [Rule::excludeIf(! $processing), 'nullable', 'integer', 'min:0', 'max:9999999999999999'],
];
}
protected function prepareForValidation(): void
{
$premium = $this->input('premium_harga_pascapanen');
if (is_string($premium) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $premium)) {
$this->merge(['premium_harga_pascapanen' => str_replace('.', '', $premium)]);
}
$prices = $this->input('harga_beli_petani');
if (! is_array($prices)) {
return;
}
foreach ($prices as &$row) {
if (! is_array($row)) {
continue;
}
foreach ($row as &$value) {
if (is_string($value) && preg_match('/^\d{1,3}(\.\d{3})+$/D', $value)) {
$value = str_replace('.', '', $value);
}
}
unset($value);
}
unset($row);
$this->merge(['harga_beli_petani' => $prices]);
}
public function after(): array
{
return [function (Validator $validator) {
$channels = $this->input('saluran_penjualan');
if ($this->boolean('melakukan_jual_beli') && is_array($channels) && count($channels) > 1 && in_array(SaluranPenjualanBumdes::BELUM_MENJUAL->value, $channels, true)) {
$validator->errors()->add('saluran_penjualan', 'Belum pernah menjual tidak boleh dipilih bersama saluran penjualan lainnya.');
}
}];
}
public function messages(): array
{
return [...parent::messages(),
'melakukan_pengolahan_pascapanen.required' => 'Kegiatan pengolahan pascapanen wajib dipilih.',
'melakukan_jual_beli.required' => 'Kegiatan jual beli komoditas wajib dipilih.',
'model_fermentasi.min' => 'Pilih minimal satu model fermentasi.',
'premium_harga_pascapanen.max' => 'Premium harga tidak boleh melebihi Rp 9.999.999.999.999.999 per kg.',
'numeric' => ':attribute harus berupa angka.',
'decimal' => ':attribute boleh memiliki maksimal 3 angka di belakang koma.',
'distinct' => ':attribute tidak boleh dipilih lebih dari satu kali.',
'harga_beli_petani.*.*.max' => 'Harga beli tidak boleh melebihi Rp 9.999.999.999.999.999 per kg.',
];
}
public function attributes(): array
{
return [...parent::attributes(),
'melakukan_pengolahan_pascapanen' => 'Kegiatan pengolahan pascapanen',
'status_sortasi' => 'Praktik sortasi',
'kriteria_sortasi' => 'Standar/kriteria sortasi',
'status_pengeringan' => 'Praktik pengeringan',
'cara_tempat_pengeringan' => 'Cara dan tempat pengeringan',
'status_fermentasi' => 'Praktik fermentasi/pengepresan',
'model_fermentasi' => 'Model fermentasi',
'proses_fermentasi' => 'Proses fermentasi',
'premium_harga_pascapanen' => 'Premium harga per kg',
'model_fermentasi.*' => 'Pilihan model fermentasi',
'melakukan_jual_beli' => 'Kegiatan jual beli komoditas',
'redirect_fragment' => 'Tujuan setelah penyimpanan',
'sumber_pasokan' => 'Sumber pasokan', 'sumber_pasokan.*' => 'Pilihan sumber pasokan',
'sumber_pasokan_lainnya' => 'Sumber pasokan lainnya',
'bentuk_produk_dibeli' => 'Bentuk produk yang dibeli', 'bentuk_produk_dibeli.*' => 'Pilihan bentuk produk yang dibeli',
'volume_pembelian' => 'Volume pembelian', 'volume_pembelian.*' => 'Volume pembelian pada musim panen terkait',
'saluran_penjualan' => 'Saluran penjualan', 'saluran_penjualan.*' => 'Pilihan saluran penjualan',
'saluran_penjualan_lainnya' => 'Saluran penjualan lainnya',
'bentuk_produk_dijual' => 'Bentuk produk yang dijual', 'bentuk_produk_dijual.*' => 'Pilihan bentuk produk yang dijual',
'volume_penjualan' => 'Volume penjualan', 'volume_penjualan.*' => 'Volume penjualan pada musim panen terkait',
'hubungan_petani_pemasok' => 'Hubungan dengan petani pemasok', 'hubungan_petani_pemasok.*' => 'Pilihan hubungan dengan petani pemasok',
'hubungan_petani_pemasok_lainnya' => 'Hubungan dengan petani pemasok lainnya',
'mekanisme_pembayaran_petani' => 'Mekanisme pembayaran kepada petani', 'mekanisme_pembayaran_lainnya' => 'Mekanisme pembayaran lainnya',
'perjanjian_petani' => 'Perjanjian dengan petani',
'harga_beli_petani' => 'Harga beli dari petani', 'harga_beli_petani.*' => 'Harga beli untuk bentuk produk terkait', 'harga_beli_petani.*.*' => 'Harga beli pada musim panen terkait',
'penetapan_harga_beli' => 'Penetapan harga beli', 'penetapan_harga_beli_lainnya' => 'Penetapan harga beli lainnya',
];
}
}
@@ -0,0 +1,5 @@
<?php
namespace App\Http\Requests\Bumdes;
class UpdatePengurusBumdesRequest extends StorePengurusBumdesRequest {}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests\Konfigurasi;
use App\Http\Controllers\Web\Konfigurasi\LogAktivitasController;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class FilterLogAktivitasRequest extends FormRequest
{
public function authorize(): bool
{
return Gate::any(['super_admin', 'admin']);
}
public function rules(): array
{
return [
'jenis' => ['nullable', Rule::in(array_keys(LogAktivitasController::JENIS))],
'search' => ['nullable', 'string', 'max:100'],
'dari' => ['nullable', 'date'],
'sampai' => ['nullable', 'date', 'after_or_equal:dari'],
'per_page' => ['nullable', 'integer', Rule::in(LogAktivitasController::PER_PAGE)],
];
}
public function messages(): array
{
return [
'sampai.after_or_equal' => 'Tanggal akhir tidak boleh sebelum tanggal awal.',
'per_page.in' => 'Jumlah data per halaman tidak valid.',
];
}
public function attributes(): array
{
return [
'jenis' => 'jenis aktivitas',
'search' => 'kata kunci',
'dari' => 'tanggal awal',
'sampai' => 'tanggal akhir',
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests\Produktivitas;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class IndexKelompokTaniRequest extends FormRequest
{
public function authorize(): bool
{
return Gate::any(['super_admin', 'admin', 'enumerator']);
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'kabupaten_kota_id' => ['bail', 'nullable', 'integer', 'exists:master_kabupaten_kota,id'],
'kecamatan_id' => ['bail', 'nullable', 'integer', 'exists:master_kecamatan,id'],
'desa_kelurahan_id' => ['bail', 'nullable', 'integer', 'exists:master_desa_kelurahan,id'],
'komoditas_id' => ['bail', 'nullable', 'integer', 'exists:master_komoditas,id'],
'jenis_kelompok' => ['nullable', 'string', 'max:50'],
'status_legalitas' => ['nullable', 'string', 'max:50'],
'sort_by' => ['nullable', 'string', 'max:50'],
'per_page' => ['nullable', 'integer', 'in:10,25'],
];
}
public function messages(): array
{
return [
'integer' => ':attribute harus berupa angka bulat.',
'exists' => ':attribute tidak ditemukan.',
'string' => ':attribute harus berupa teks.',
'max' => ':attribute terlalu panjang.',
'in' => 'Pilihan :attribute tidak valid.',
];
}
public function attributes(): array
{
return [
'search' => 'Pencarian',
'kabupaten_kota_id' => 'Kabupaten/Kota',
'kecamatan_id' => 'Kecamatan',
'desa_kelurahan_id' => 'Desa/Kelurahan',
'komoditas_id' => 'Komoditas',
'jenis_kelompok' => 'Jenis kelompok',
'status_legalitas' => 'Status SK/Dokumen Pendaftaran',
'sort_by' => 'Urutan data',
'per_page' => 'Jumlah data per halaman',
];
}
}

Some files were not shown because too many files have changed in this diff Show More