Compare commits
166
Commits
2c28bb06db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd93e0d265 | ||
|
|
8ef4ae1f15 | ||
|
|
89c5a1371a | ||
|
|
41da3a71ee | ||
|
|
b5b046ed86 | ||
|
|
aa91c3fe3f | ||
|
|
53913bebe9 | ||
|
|
f455f8b878 | ||
|
|
f1112e3b68 | ||
|
|
c1370a26ca | ||
|
|
013cb7b728 | ||
|
|
4ab745cfe5 | ||
|
|
c66eed56e2 | ||
|
|
8be0906a2e | ||
|
|
fcd79b7cfe | ||
|
|
bea9be8a39 | ||
|
|
5220b10de7 | ||
|
|
24725e3df1 | ||
|
|
60d32a88e9 | ||
|
|
4370d3c3d0 | ||
|
|
7699430301 | ||
|
|
df2b5a1312 | ||
|
|
2e8271d27a | ||
|
|
9b5c0a7309 | ||
|
|
9d56b67998 | ||
|
|
f95f43999a | ||
|
|
3e172cf915 | ||
|
|
3a9b3dbd7b | ||
|
|
80f0056efe | ||
|
|
aef27704f2 | ||
|
|
f49bb0c2db | ||
|
|
8a2c0292ac | ||
|
|
9ea324e553 | ||
|
|
94c3288ea7 | ||
|
|
8539bdcec4 | ||
|
|
77de648ded | ||
|
|
393cf0d0d1 | ||
|
|
7fdf9b400d | ||
|
|
be4d46004e | ||
|
|
fef5358402 | ||
|
|
02eed6da34 | ||
|
|
94d3f2a582 | ||
|
|
471ca5ac0e | ||
|
|
8ec8f0c9d6 | ||
|
|
4d7d16e9f6 | ||
|
|
1155b9b95b | ||
|
|
607533a327 | ||
|
|
6b25bff2c4 | ||
|
|
71b4d42ea2 | ||
|
|
c42a2571f5 | ||
|
|
16903b21e3 | ||
|
|
1d4a44b8ac | ||
|
|
539239b507 | ||
|
|
a76b014056 | ||
|
|
d50c292cf7 | ||
|
|
dacaec7a86 | ||
|
|
6967eafe98 | ||
|
|
118cda625a | ||
|
|
a3f7cb4669 | ||
|
|
ec009a2c55 | ||
|
|
b63dafdaee | ||
|
|
b97f682cd3 | ||
|
|
404019bab6 | ||
|
|
609919ba30 | ||
|
|
047cb4a779 | ||
|
|
530c297b53 | ||
|
|
8fbf68e6ef | ||
|
|
ac8b370248 | ||
|
|
88762f7afb | ||
|
|
b96de514e3 | ||
|
|
6830142a82 | ||
|
|
4b26e82afb | ||
|
|
b855a857b2 | ||
|
|
ba7520a23c | ||
|
|
0b333ab4e9 | ||
|
|
3b1f15d5b3 | ||
|
|
605506bba8 | ||
|
|
c960ba14b1 | ||
|
|
a4f38998d5 | ||
|
|
0d9f6c0b64 | ||
|
|
81775b0c13 | ||
|
|
ba1a318d09 | ||
|
|
0761d64254 | ||
|
|
67c3c6c809 | ||
|
|
04387268cd | ||
|
|
946fabb9a5 | ||
|
|
0a0c86d648 | ||
|
|
8b21fab8b8 | ||
|
|
16a0a3c56c | ||
|
|
d4c6b64ba6 | ||
|
|
b4089d72b3 | ||
|
|
01990ccec1 | ||
|
|
8aed88a31f | ||
|
|
4aa0780802 | ||
|
|
4e41246382 | ||
|
|
43fe9e5f7f | ||
|
|
c8d1994d3b | ||
|
|
cf0f4b47b4 | ||
|
|
24a8016710 | ||
|
|
20e1eee6ff | ||
|
|
c0432cd5f8 | ||
|
|
0ce0936a80 | ||
|
|
502c932f21 | ||
|
|
e60a66eeb7 | ||
|
|
4475cd0098 | ||
|
|
67c111a2b3 | ||
|
|
3bcaa3ea50 | ||
|
|
bed9a6f620 | ||
|
|
712b10e405 | ||
|
|
6cfcf46ba6 | ||
|
|
b1b9828514 | ||
|
|
7af7e1bdaf | ||
|
|
bd05fc7f0f | ||
|
|
17b5687db8 | ||
|
|
e7f48b6500 | ||
|
|
a5884d0f8c | ||
|
|
9576c6295e | ||
|
|
079e300d5d | ||
|
|
2c94316439 | ||
|
|
690b53e5a2 | ||
|
|
50c9adfdc7 | ||
|
|
696c7dede8 | ||
|
|
5967e7d6f3 | ||
|
|
4c66d72dc5 | ||
|
|
c43483a9f8 | ||
|
|
e02a34a527 | ||
|
|
713355ce0a | ||
|
|
12f8d0bbda | ||
|
|
8afe7f9b38 | ||
|
|
fbf84eb4cb | ||
|
|
e04093ad3f | ||
|
|
3e1d3a33ee | ||
|
|
e316d064ef | ||
|
|
0c289ec244 | ||
|
|
089f1b515d | ||
|
|
4c647ea8ce | ||
|
|
419404e273 | ||
|
|
837e542851 | ||
|
|
9378d15d71 | ||
|
|
0381e9bf0e | ||
|
|
3a70445e9a | ||
|
|
d2ed2c58fd | ||
|
|
9b201efa06 | ||
|
|
2b3a0b7c58 | ||
|
|
a187fb1095 | ||
|
|
6b44c11181 | ||
|
|
090153e53a | ||
|
|
a79a93d291 | ||
|
|
62f0f1bdbc | ||
|
|
f0acd8c7ed | ||
|
|
3908b0be71 | ||
|
|
05cc3be9d3 | ||
|
|
f4c83d348a | ||
|
|
61bd8dc388 | ||
|
|
002d6044b4 | ||
|
|
30196278d7 | ||
|
|
08c19c0714 | ||
|
|
d6c036e19a | ||
|
|
981bc0d494 | ||
|
|
857da1e900 | ||
|
|
66fc7227e6 | ||
|
|
3edfc57ba7 | ||
|
|
4a560ebc4e | ||
|
|
d4eb27e583 | ||
|
|
d1c0f90afd | ||
|
|
a1dadd1b1b |
@@ -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=
|
||||
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
# 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.
|
||||
|
||||
### 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 | Belum | |
|
||||
| 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
|
||||
- [ ] 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
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\AksesPasar;
|
||||
|
||||
enum TingkatKepercayaanPembeli: int
|
||||
{
|
||||
case SANGAT_PERCAYA = 1;
|
||||
case CUKUP_PERCAYA = 2;
|
||||
case RAGU_RAGU = 3;
|
||||
case TIDAK_PERCAYA = 4;
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return [
|
||||
['value' => self::SANGAT_PERCAYA->value, 'label' => 'Sangat percaya, berani langsung coba'],
|
||||
['value' => self::CUKUP_PERCAYA->value, 'label' => 'Cukup percaya, tapi hati hati'],
|
||||
['value' => self::RAGU_RAGU->value, 'label' => 'Ragu ragu'],
|
||||
['value' => self::TIDAK_PERCAYA->value, 'label' => 'Tidak percaya, enggan mencoba'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Bumdes;
|
||||
|
||||
enum JabatanBumdes: string
|
||||
{
|
||||
case DIREKTUR = 'direktur';
|
||||
case DIREKTUR_KETUA = 'direktur_ketua';
|
||||
case SEKRETARIS = 'sekretaris';
|
||||
case BENDAHARA = 'bendahara';
|
||||
case KEPALA_UNIT_USAHA = 'kepala_unit_usaha';
|
||||
case PENGAWAS = 'pengawas';
|
||||
case PENASIHAT = 'penasihat';
|
||||
case ANGGOTA = 'anggota';
|
||||
case STAF_OPERASIONAL = 'staf_operasional';
|
||||
case LAINNYA = 'lainnya';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DIREKTUR => 'Direktur/Ketua',
|
||||
self::DIREKTUR_KETUA => 'Direktur/Ketua',
|
||||
self::SEKRETARIS => 'Sekretaris',
|
||||
self::BENDAHARA => 'Bendahara',
|
||||
self::KEPALA_UNIT_USAHA => 'Kepala Unit Usaha',
|
||||
self::PENGAWAS => 'Pengawas',
|
||||
self::PENASIHAT => 'Penasihat',
|
||||
self::ANGGOTA => 'Anggota',
|
||||
self::STAF_OPERASIONAL => 'Staf Operasional',
|
||||
self::LAINNYA => 'Lainnya',
|
||||
};
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return collect([
|
||||
self::DIREKTUR_KETUA,
|
||||
self::SEKRETARIS,
|
||||
self::BENDAHARA,
|
||||
self::LAINNYA,
|
||||
])->map(fn ($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ enum KeparahanSerangan: string
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::RINGAN => 'Ringan',
|
||||
self::SEDANG => 'Sedang',
|
||||
self::BERAT => 'Berat',
|
||||
self::RINGAN => 'Ringan, hanya beberapa tanaman',
|
||||
self::SEDANG => 'Sedang, sebagian tanaman terdampak',
|
||||
self::BERAT => 'Parah, sebagian besar tanaman terdampak',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ enum KondisiTanaman: string
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::BAIK => 'Baik',
|
||||
self::SEDANG => 'Sedang',
|
||||
self::BURUK => 'Buruk',
|
||||
self::MATI => 'Mati',
|
||||
self::BAIK => 'Baik, tumbuh lebat, banyak buah',
|
||||
self::SEDANG => 'Sedang, cukup tumbuh, ada masalah',
|
||||
self::BURUK => 'Buruk, banyak tanaman tua dan lemah/tidak berbuah',
|
||||
self::MATI => 'Mati, tanaman telah mati, tidak dapat tumbuh maupun berbuah',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums\Koperasi;
|
||||
|
||||
enum PosisiKoperasi: string
|
||||
{
|
||||
case PENGURUS = 'Pengurus koperasi';
|
||||
case ANGGOTA = 'Anggota koperasi';
|
||||
case PENDAMPING = 'Pendamping koperasi';
|
||||
}
|
||||
@@ -19,13 +19,13 @@ enum Pendidikan: string
|
||||
{
|
||||
return match($this) {
|
||||
self::TIDAK_SEKOLAH => 'Tidak Sekolah',
|
||||
self::SD => 'SD atau Sederajat',
|
||||
self::SMP => 'SMP atau Sederajat',
|
||||
self::SMA => 'SMA atau Sederajat',
|
||||
self::SD => 'SD',
|
||||
self::SMP => 'SMP',
|
||||
self::SMA => 'SMA/SMK',
|
||||
self::DIPLOMA => 'Diploma 1-3 (D1-D3)',
|
||||
self::SARJANA => 'Sarjana (S1) atau sederajat',
|
||||
self::MAGISTER => 'Magister (S2) atau sederajat',
|
||||
self::DOKTOR => 'Doktor (S3) atau sederajat',
|
||||
self::SARJANA => 'Sarjana (S1)',
|
||||
self::MAGISTER => 'Magister (S2)',
|
||||
self::DOKTOR => 'Doktor (S3)',
|
||||
self::PESANTREN => 'Pesantren, Semiari, Wihara, atau Pendidikan Keagamaan Formal',
|
||||
self::LAINNYA => 'Lainnya',
|
||||
};
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -23,4 +24,4 @@ enum PelatihanGAP: string
|
||||
'label' => $case->label(),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ enum FrekuensiPemangkasanPendamping: string
|
||||
case TIDAK_RUTIN = 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya';
|
||||
case SATU_KALI_TAHUN = 'Memangkas 1 kali/tahun, tahu jenis tekniknya';
|
||||
case DUA_KALI_TAHUN = 'Memangkas 2 kali/tahun, tahu jenis tekniknya';
|
||||
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin';
|
||||
case LEBIH_DUA_KALI_TAHUN = 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanPendamping: string
|
||||
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
|
||||
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
||||
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
|
||||
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
|
||||
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanUtama: int
|
||||
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
|
||||
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
||||
self::DUA_KALI_TAHUN => 'Memangkas 2 kali/tahun, tahu jenis tekniknya',
|
||||
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat dan pohon penaung juga dipangkas rutin',
|
||||
self::LEBIH_DUA_KALI_TAHUN => 'Memangkas lebih dari 2 kali/tahun dengan teknik yang tepat, dan pohon penaung juga dipangkas rutin',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ enum MetodePenangananHamaPenyakit: int
|
||||
self::TIDAK_ADA => 'Tidak ada penanganan',
|
||||
self::CARA_TRADISIONAL => 'Cara tradisional (doa/ritual)',
|
||||
self::PESTISIDA_SANITASI => 'Pestisida atau sanitasi',
|
||||
self::KOMBINASI => 'Kombinasi pestisada dan sanitasi',
|
||||
self::PHT => 'Pengendalian hama terpadu(PHT)',
|
||||
self::KOMBINASI => 'Kombinasi pestisida dan sanitasi',
|
||||
self::PHT => 'Pengendalian hama terpadu (PHT)',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -2,11 +2,26 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum Role: string
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ use App\Enums\StatusLahan;
|
||||
|
||||
class LahanController extends Controller
|
||||
{
|
||||
/**
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
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,24 +91,25 @@ 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'],
|
||||
'status_kepemilikan' => ['required', new Enum(StatusLahan::class)],
|
||||
'desa_kelurahan_id' => ['nullable', 'numeric', 'exists:master_desa_kelurahan,id'],
|
||||
'path' => ['nullable', 'array'],
|
||||
'path.*' => ['array'],
|
||||
'path' => ['nullable', 'string'],
|
||||
'luas_lahan' => ['nullable', 'numeric'],
|
||||
'uid' => ['required', 'string'],
|
||||
'profile_id' => $profileRule,
|
||||
'foto' => ['nullable', 'array'],
|
||||
'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) {
|
||||
@@ -116,14 +117,26 @@ class LahanController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$desaKelurahanId = $user->role === 'fasilitator'
|
||||
$desaKelurahanId = $user->role === 'enumerator'
|
||||
? $profile->desa_kelurahan_id
|
||||
: ($validated['desa_kelurahan_id'] ?? $profile->desa_kelurahan_id);
|
||||
|
||||
$fotoPaths = [];
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$fotoPaths[] = $file->store('foto_lahan', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
$lahan = Lahan::create([
|
||||
...$validated,
|
||||
'profile_id' => $profile->id,
|
||||
'nama' => $validated['nama'],
|
||||
'status_kepemilikan' => $validated['status_kepemilikan'],
|
||||
'desa_kelurahan_id' => $desaKelurahanId,
|
||||
'path' => $validated['path'] ?? null,
|
||||
'luas_lahan' => $validated['luas_lahan'] ?? null,
|
||||
'uid' => $validated['uid'],
|
||||
'profile_id' => $profile->id,
|
||||
'foto' => $fotoPaths,
|
||||
]);
|
||||
|
||||
return (new LahanResource($lahan))
|
||||
@@ -136,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);
|
||||
}
|
||||
|
||||
@@ -153,16 +166,16 @@ class LahanController extends Controller
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -173,9 +186,26 @@ class LahanController extends Controller
|
||||
'path' => ['nullable', 'array'],
|
||||
'path.*' => ['array'],
|
||||
'luas_lahan' => ['nullable', 'numeric'],
|
||||
'foto' => ['nullable', 'array'],
|
||||
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'],
|
||||
]);
|
||||
|
||||
$lahan->update($validated);
|
||||
$fotoPaths = $lahan->foto ?? [];
|
||||
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$fotoPaths[] = $file->store('foto_lahan', 'public');
|
||||
}
|
||||
}
|
||||
|
||||
$lahan->update([
|
||||
'nama' => $validated['nama'],
|
||||
'status_kepemilikan' => $validated['status_kepemilikan'],
|
||||
'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? $lahan->desa_kelurahan_id,
|
||||
'path' => $validated['path'] ?? null,
|
||||
'luas_lahan' => $validated['luas_lahan'] ?? null,
|
||||
'foto' => $fotoPaths,
|
||||
]);
|
||||
|
||||
return (new LahanResource($lahan))->response();
|
||||
}
|
||||
@@ -185,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);
|
||||
}
|
||||
|
||||
@@ -203,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,580 @@
|
||||
<?php
|
||||
|
||||
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\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;
|
||||
|
||||
class PengurusBumdesController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
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 === 'enumerator') {
|
||||
$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'
|
||||
));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$provinsi = $this->provinsiOptions();
|
||||
$jabatan = JabatanBumdes::options();
|
||||
$pendidikan = Pendidikan::options();
|
||||
|
||||
return view('bumdes.profil.add', compact('provinsi', 'jabatan', 'pendidikan'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$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.');
|
||||
}
|
||||
|
||||
public function show(string $id)
|
||||
{
|
||||
return redirect()->route('bumdes.edit', $id);
|
||||
}
|
||||
|
||||
public function edit(string $id)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
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'));
|
||||
}
|
||||
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$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.');
|
||||
}
|
||||
|
||||
public function destroy(string $id)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$pengurus = $this->pengurusQuery()->findOrFail($id);
|
||||
$pengurus->update(['deleted_by' => auth()->id()]);
|
||||
$pengurus->delete();
|
||||
|
||||
return response()->json(['message' => 'Data pengurus BUMDes berhasil dihapus.']);
|
||||
}
|
||||
|
||||
public function kelembagaanAktor(string $id)
|
||||
{
|
||||
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$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', 'enumerator'])) {
|
||||
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 === 'enumerator') {
|
||||
$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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\PendapatanKomoditas;
|
||||
use App\Models\Pertanian\SosialEkonomi;
|
||||
|
||||
class DemografiPetaniController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return redirect()->route('dashboard.karakteristik-petani', array_merge($request->query(), [
|
||||
'tab' => 'demografi-petani',
|
||||
]));
|
||||
}
|
||||
|
||||
public function karakteristik(Request $request)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$activeTab = $request->query('tab') === 'indeks-petani'
|
||||
? 'indeks-petani'
|
||||
: 'demografi-petani';
|
||||
|
||||
$demografiData = $this->contentData($request);
|
||||
$indeksData = app(IndeksPetaniController::class)->contentData($request);
|
||||
|
||||
$tableFilters = [
|
||||
'gender' => ['Gender', ['1' => 'Laki-laki', '0' => 'Perempuan']],
|
||||
'umur' => ['Kelompok Umur', ['kurang_30' => '< 30 tahun', '30_39' => '30–39 tahun', '40_49' => '40–49 tahun', '50_59' => '50–59 tahun', '60_keatas' => '≥ 60 tahun']],
|
||||
'pendidikan' => ['Pendidikan', ['tidak_sekolah' => 'Tidak Sekolah', 'sd' => 'SD', 'smp' => 'SMP', 'sma' => 'SMA', 'diploma' => 'Diploma', 's1_keatas' => 'Sarjana ke atas']],
|
||||
'jumlah_tanggungan' => ['Jumlah Tanggungan', ['0' => '0 orang', '1_2' => '1–2 orang', '3_4' => '3–4 orang', '5_keatas' => '≥ 5 orang']],
|
||||
'kelompok_tani' => ['Keanggotaan Kelompok Tani', ['terdaftar' => 'Terdaftar', 'tidak_terdaftar' => 'Tidak Terdaftar', 'belum_terisi' => 'Belum Terisi']],
|
||||
'komoditas' => ['Komoditas', $indeksData['komoditas']->pluck('nama', 'id')->all()],
|
||||
'luas_lahan' => ['Luas Lahan', ['kurang_05' => '< 0,5 ha', '05_1' => '0,5–1 ha', '1_2' => '> 1–2 ha', 'lebih_2' => '> 2 ha']],
|
||||
'status_kepemilikan_lahan' => ['Status Kepemilikan Lahan', ['sendiri' => 'Milik Pribadi', 'sewa' => 'Sewa', 'adat' => 'Tanah Adat']],
|
||||
'penghasilan' => ['Rentang Pendapatan', ['kurang_1' => '< Rp1 juta', '1_3' => 'Rp1–3 juta', '3_5' => '> Rp3–5 juta', '5_10' => '> Rp5–10 juta', 'lebih_10' => '> Rp10 juta']],
|
||||
];
|
||||
|
||||
return view('dashboard.karakteristik-petani', [
|
||||
'activeTab' => $activeTab,
|
||||
'tableFilters' => $tableFilters,
|
||||
'dashboardFilters' => $request->only([
|
||||
'provinsi_id',
|
||||
'kabupatenKota_id',
|
||||
'kecamatan_id',
|
||||
'desaKelurahan_id',
|
||||
'komoditas_id',
|
||||
'segmen',
|
||||
]),
|
||||
'indeksFilterAction' => route('dashboard.karakteristik-petani'),
|
||||
] + $demografiData + $indeksData);
|
||||
}
|
||||
|
||||
public function contentData(Request $request): array
|
||||
{
|
||||
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
|
||||
|
||||
$petaniCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)->selectRaw("
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE jenis_kelamin = true) as laki,
|
||||
COUNT(*) FILTER (WHERE jenis_kelamin = false) as perempuan
|
||||
")->first();
|
||||
|
||||
$totalPetani = $petaniCounts->total;
|
||||
$petaniLakilaki = $petaniCounts->laki;
|
||||
$petaniPerempuan = $petaniCounts->perempuan;
|
||||
|
||||
$totalKomoditas = Komoditas::query()
|
||||
->where('is_utama', true)
|
||||
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
|
||||
->count();
|
||||
|
||||
$totalLuasLahan = Lahan::has('petani')
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->sum('luas_lahan');
|
||||
$ratarataLuasLahan = $totalLuasLahan && $totalPetani ? $totalLuasLahan / $totalPetani : 0;
|
||||
|
||||
$umurCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)
|
||||
->whereNotNull('tanggal_lahir')->selectRaw("
|
||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) < 30) as kurang_30,
|
||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 30 AND 39) as umur_30_39,
|
||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 40 AND 49) as umur_40_49,
|
||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 50 AND 59) as umur_50_59,
|
||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) >= 60) as umur_60_keatas
|
||||
")->first();
|
||||
|
||||
$pendidikanCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)
|
||||
->whereNotNull('pendidikan_terakhir')
|
||||
->select('pendidikan_terakhir', DB::raw('COUNT(*) as total'))
|
||||
->groupBy('pendidikan_terakhir')
|
||||
->pluck('total', 'pendidikan_terakhir');
|
||||
|
||||
$pendidikan = [
|
||||
'tidak_sekolah' => (int) ($pendidikanCounts['01_tidak_sekolah'] ?? 0),
|
||||
'sd' => (int) ($pendidikanCounts['02_sd'] ?? 0),
|
||||
'smp' => (int) ($pendidikanCounts['03_smp'] ?? 0),
|
||||
'sma' => (int) ($pendidikanCounts['04_sma'] ?? 0),
|
||||
'diploma' => (int) ($pendidikanCounts['05_diploma'] ?? 0),
|
||||
's1_keatas' => (int) ($pendidikanCounts['06_sarjana'] ?? 0) + (int) ($pendidikanCounts['07_magister'] ?? 0),
|
||||
];
|
||||
|
||||
$jumlahTanggunganBuckets = SosialEkonomi::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->whereNotNull('jumlah_tanggungan')
|
||||
->selectRaw("
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan = 0) as tanggungan_0,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan BETWEEN 1 AND 2) as tanggungan_1_2,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan BETWEEN 3 AND 4) as tanggungan_3_4,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE jumlah_tanggungan >= 5) as tanggungan_5_keatas
|
||||
")
|
||||
->first();
|
||||
|
||||
$jumlahTanggungan = [
|
||||
'0' => (int) ($jumlahTanggunganBuckets->tanggungan_0 ?? 0),
|
||||
'1_2' => (int) ($jumlahTanggunganBuckets->tanggungan_1_2 ?? 0),
|
||||
'3_4' => (int) ($jumlahTanggunganBuckets->tanggungan_3_4 ?? 0),
|
||||
'5_keatas' => (int) ($jumlahTanggunganBuckets->tanggungan_5_keatas ?? 0),
|
||||
];
|
||||
|
||||
$kelompokTaniBuckets = Petani::query()
|
||||
->whereIn('petani.id', clone $filteredPetaniIds)
|
||||
->leftJoin('sosial_ekonomi', function ($join) {
|
||||
$join->on('sosial_ekonomi.petani_id', '=', 'petani.id')
|
||||
->whereNull('sosial_ekonomi.deleted_at');
|
||||
})
|
||||
->selectRaw("
|
||||
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani = 'ya') as terdaftar,
|
||||
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani = 'tidak') as tidak_terdaftar,
|
||||
COUNT(DISTINCT petani.id) FILTER (WHERE sosial_ekonomi.terdaftar_kelompok_tani IS NULL OR sosial_ekonomi.terdaftar_kelompok_tani NOT IN ('ya', 'tidak')) as belum_terisi
|
||||
")
|
||||
->first();
|
||||
|
||||
$kelompokTani = [
|
||||
'terdaftar' => (int) ($kelompokTaniBuckets->terdaftar ?? 0),
|
||||
'tidak_terdaftar' => (int) ($kelompokTaniBuckets->tidak_terdaftar ?? 0),
|
||||
'belum_terisi' => (int) ($kelompokTaniBuckets->belum_terisi ?? 0),
|
||||
];
|
||||
|
||||
$desaSasaranRows = Petani::query()
|
||||
->leftJoin('master_desa_kelurahan as desa', 'petani.desa_kelurahan_id', '=', 'desa.id')
|
||||
->whereIn('petani.id', clone $filteredPetaniIds)
|
||||
->selectRaw('petani.desa_kelurahan_id as desa_id')
|
||||
->selectRaw("COALESCE(desa.nama, 'Tidak diketahui') as desa")
|
||||
->selectRaw('COUNT(*) as total')
|
||||
->groupBy('petani.desa_kelurahan_id', 'desa.nama')
|
||||
->orderByDesc('total')
|
||||
->orderBy('desa')
|
||||
->get();
|
||||
|
||||
$komoditasUtama = Komoditas::query()
|
||||
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
|
||||
->withCount(['petani' => fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds)])
|
||||
->orderByDesc('petani_count')
|
||||
->get(['id', 'nama']);
|
||||
|
||||
$luasLahanBuckets = DB::query()->fromSub(
|
||||
Lahan::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->select('petani_id')
|
||||
->selectRaw('luas_lahan'),
|
||||
'agg'
|
||||
)->selectRaw("
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan < 0.5) as kurang_05,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan >= 0.5 AND luas_lahan <= 1) as luas_05_1,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 1 AND luas_lahan <= 2) as luas_1_2,
|
||||
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 2) as lebih_2
|
||||
")->first();
|
||||
|
||||
$luasLahan = [
|
||||
'kurang_05' => $luasLahanBuckets->kurang_05,
|
||||
'05_1' => $luasLahanBuckets->luas_05_1,
|
||||
'1_2' => $luasLahanBuckets->luas_1_2,
|
||||
'lebih_2' => $luasLahanBuckets->lebih_2,
|
||||
];
|
||||
|
||||
$statusKepemilikanLahanRows = Lahan::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->whereNotNull('status_kepemilikan')
|
||||
->selectRaw('status_kepemilikan as status')
|
||||
->selectRaw('COUNT(DISTINCT petani_id) as total')
|
||||
->groupBy('status_kepemilikan')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
$pendapatanPerPetaniQuery = PendapatanKomoditas::query()
|
||||
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
||||
->whereNotNull('sosial_ekonomi.petani_id')
|
||||
->whereIn('sosial_ekonomi.petani_id', clone $filteredPetaniIds)
|
||||
->groupBy('sosial_ekonomi.petani_id')
|
||||
->selectRaw('SUM(pendapatan_komoditas.pendapatan) as total_pendapatan');
|
||||
|
||||
$penghasilanBuckets = DB::query()->fromSub(
|
||||
clone $pendapatanPerPetaniQuery,
|
||||
'agg'
|
||||
)->selectRaw("
|
||||
AVG(total_pendapatan) as rata_rata,
|
||||
COUNT(*) FILTER (WHERE total_pendapatan < 1000000) as kurang_1,
|
||||
COUNT(*) FILTER (WHERE total_pendapatan >= 1000000 AND total_pendapatan <= 3000000) as p1_3,
|
||||
COUNT(*) FILTER (WHERE total_pendapatan > 3000000 AND total_pendapatan <= 5000000) as p3_5,
|
||||
COUNT(*) FILTER (WHERE total_pendapatan > 5000000 AND total_pendapatan <= 10000000) as p5_10,
|
||||
COUNT(*) FILTER (WHERE total_pendapatan > 10000000) as lebih_10
|
||||
")->first();
|
||||
|
||||
$ratarataPenghasilan = (float) ($penghasilanBuckets->rata_rata ?? 0);
|
||||
|
||||
$penghasilan = [
|
||||
'kurang_1' => $penghasilanBuckets->kurang_1,
|
||||
'1_3' => $penghasilanBuckets->p1_3,
|
||||
'3_5' => $penghasilanBuckets->p3_5,
|
||||
'5_10' => $penghasilanBuckets->p5_10,
|
||||
'lebih_10' => $penghasilanBuckets->lebih_10,
|
||||
];
|
||||
|
||||
$data = [
|
||||
'total_petani' => $totalPetani,
|
||||
'petani_lakilaki' => $petaniLakilaki,
|
||||
'petani_perempuan' => $petaniPerempuan,
|
||||
'total_komoditas' => $totalKomoditas,
|
||||
'total_luas_lahan' => $totalLuasLahan,
|
||||
'ratarata_luas_lahan' => $ratarataLuasLahan,
|
||||
'ratarata_penghasilan' => $ratarataPenghasilan,
|
||||
'kelompok_umur' => [
|
||||
'kurang_30' => $umurCounts->kurang_30,
|
||||
'30_39' => $umurCounts->umur_30_39,
|
||||
'40_49' => $umurCounts->umur_40_49,
|
||||
'50_59' => $umurCounts->umur_50_59,
|
||||
'60_keatas' => $umurCounts->umur_60_keatas,
|
||||
],
|
||||
'pendidikan' => $pendidikan,
|
||||
'jumlah_tanggungan' => $jumlahTanggungan,
|
||||
'kelompok_tani' => $kelompokTani,
|
||||
'desa_sasaran' => [
|
||||
'total' => $desaSasaranRows->count(),
|
||||
'ids' => $desaSasaranRows->pluck('desa_id')->values(),
|
||||
'labels' => $desaSasaranRows->pluck('desa')->values(),
|
||||
'series' => $desaSasaranRows->pluck('total')->map(fn ($v) => (int) $v)->values(),
|
||||
],
|
||||
'komoditas_utama' => [
|
||||
'ids' => $komoditasUtama->pluck('id')->values(),
|
||||
'labels' => $komoditasUtama->pluck('nama')->values(),
|
||||
'series' => $komoditasUtama->pluck('petani_count')->map(fn ($v) => (int) $v)->values(),
|
||||
],
|
||||
'luas_lahan' => $luasLahan,
|
||||
'status_kepemilikan_lahan' => [
|
||||
'keys' => $statusKepemilikanLahanRows->pluck('status')->values(),
|
||||
'labels' => $statusKepemilikanLahanRows
|
||||
->pluck('status')
|
||||
->map(fn ($status) => $this->statusLahanLabel((string) $status))
|
||||
->values(),
|
||||
'series' => $statusKepemilikanLahanRows->pluck('total')->map(fn ($v) => (int) $v)->values(),
|
||||
],
|
||||
'penghasilan' => $penghasilan,
|
||||
];
|
||||
|
||||
return compact('data');
|
||||
}
|
||||
|
||||
private function filteredPetaniIdsQuery(Request $request)
|
||||
{
|
||||
$query = Petani::query()
|
||||
->leftJoin('master_desa_kelurahan as desa', 'petani.desa_kelurahan_id', '=', 'desa.id')
|
||||
->leftJoin('master_kecamatan as kecamatan', 'desa.kecamatan_id', '=', 'kecamatan.id')
|
||||
->leftJoin('master_kabupaten_kota as kabupaten', 'kecamatan.kabupaten_kota_id', '=', 'kabupaten.id')
|
||||
->when($request->filled('provinsi_id'), fn ($query) => $query->where('kabupaten.provinsi_id', $request->provinsi_id))
|
||||
->when($request->filled('kabupatenKota_id'), fn ($query) => $query->where('kecamatan.kabupaten_kota_id', $request->kabupatenKota_id))
|
||||
->when($request->filled('kecamatan_id'), fn ($query) => $query->where('desa.kecamatan_id', $request->kecamatan_id))
|
||||
->when($request->filled('desaKelurahan_id'), fn ($query) => $query->where('petani.desa_kelurahan_id', $request->desaKelurahan_id))
|
||||
->when($request->filled('komoditas_id'), fn ($query) => $query->where('petani.komoditas_id', $request->komoditas_id));
|
||||
|
||||
if ($request->filled('segmen')) {
|
||||
$query->whereHas('skor', function ($query) use ($request) {
|
||||
match ($request->segmen) {
|
||||
'Penggerak' => $query->where('skor_kesiapan_adopsi', '>=', 4),
|
||||
'Perintis' => $query->where('skor_kesiapan_adopsi', '>=', 3)
|
||||
->where('skor_kesiapan_adopsi', '<', 4),
|
||||
'Pemerhati' => $query->where('skor_kesiapan_adopsi', '>=', 2)
|
||||
->where('skor_kesiapan_adopsi', '<', 3),
|
||||
'Pemula' => $query->where('skor_kesiapan_adopsi', '>=', 1)
|
||||
->where('skor_kesiapan_adopsi', '<', 2),
|
||||
'Belum Lengkap' => $query->where(function ($query) {
|
||||
$query->whereNull('skor_kesiapan_adopsi')
|
||||
->orWhere('skor_kesiapan_adopsi', '<', 1);
|
||||
}),
|
||||
default => null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return $query->select('petani.id');
|
||||
}
|
||||
|
||||
private function statusLahanLabel(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'sendiri' => 'Milik Pribadi',
|
||||
'sewa' => 'Sewa',
|
||||
'adat' => 'Tanah Adat',
|
||||
default => ucwords(str_replace('_', ' ', $status)),
|
||||
};
|
||||
}
|
||||
|
||||
public function data(Request $request)
|
||||
{
|
||||
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
|
||||
$pendapatanSub = PendapatanKomoditas::query()
|
||||
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
||||
->whereColumn('sosial_ekonomi.petani_id', 'petani.id')
|
||||
->selectRaw('SUM(pendapatan_komoditas.pendapatan)');
|
||||
|
||||
$query = Petani::with('komoditas')
|
||||
->whereIn('petani.id', clone $filteredPetaniIds)
|
||||
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
||||
->addSelect(['total_pendapatan' => $pendapatanSub]);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->where('nama', 'ILIKE', "%{$request->search}%");
|
||||
}
|
||||
|
||||
if ($request->has('gender') && $request->gender !== '') {
|
||||
$query->where('jenis_kelamin', $request->gender);
|
||||
}
|
||||
|
||||
if ($request->filled('umur')) {
|
||||
match ($request->umur) {
|
||||
'kurang_30' => $query->whereNotNull('tanggal_lahir')
|
||||
->whereRaw("EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) < 30"),
|
||||
'30_39' => $query->whereNotNull('tanggal_lahir')
|
||||
->whereRaw("EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 30 AND 39"),
|
||||
'40_49' => $query->whereNotNull('tanggal_lahir')
|
||||
->whereRaw("EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 40 AND 49"),
|
||||
'50_59' => $query->whereNotNull('tanggal_lahir')
|
||||
->whereRaw("EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) BETWEEN 50 AND 59"),
|
||||
'60_keatas' => $query->whereNotNull('tanggal_lahir')
|
||||
->whereRaw("EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) >= 60"),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($request->filled('pendidikan')) {
|
||||
$pendidikanMap = [
|
||||
'tidak_sekolah' => ['01_tidak_sekolah'],
|
||||
'sd' => ['02_sd'],
|
||||
'smp' => ['03_smp'],
|
||||
'sma' => ['04_sma'],
|
||||
'diploma' => ['05_diploma'],
|
||||
's1_keatas' => ['06_sarjana', '07_magister'],
|
||||
];
|
||||
|
||||
if (isset($pendidikanMap[$request->pendidikan])) {
|
||||
$query->whereIn('pendidikan_terakhir', $pendidikanMap[$request->pendidikan]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('jumlah_tanggungan')) {
|
||||
$tanggunganQuery = SosialEkonomi::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->whereNotNull('jumlah_tanggungan')
|
||||
->select('petani_id')
|
||||
->distinct();
|
||||
|
||||
match ($request->jumlah_tanggungan) {
|
||||
'0' => $tanggunganQuery->where('jumlah_tanggungan', 0),
|
||||
'1_2' => $tanggunganQuery->whereBetween('jumlah_tanggungan', [1, 2]),
|
||||
'3_4' => $tanggunganQuery->whereBetween('jumlah_tanggungan', [3, 4]),
|
||||
'5_keatas' => $tanggunganQuery->where('jumlah_tanggungan', '>=', 5),
|
||||
default => null,
|
||||
};
|
||||
|
||||
$query->whereIn('petani.id', $tanggunganQuery);
|
||||
}
|
||||
|
||||
if ($request->filled('kelompok_tani')) {
|
||||
match ($request->kelompok_tani) {
|
||||
'terdaftar' => $query->whereIn('petani.id', SosialEkonomi::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->where('terdaftar_kelompok_tani', 'ya')
|
||||
->select('petani_id')
|
||||
->distinct()),
|
||||
'tidak_terdaftar' => $query->whereIn('petani.id', SosialEkonomi::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->where('terdaftar_kelompok_tani', 'tidak')
|
||||
->select('petani_id')
|
||||
->distinct()),
|
||||
'belum_terisi' => $query->whereNotIn('petani.id', SosialEkonomi::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->whereIn('terdaftar_kelompok_tani', ['ya', 'tidak'])
|
||||
->select('petani_id')
|
||||
->distinct()),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($request->filled('komoditas')) {
|
||||
$query->where('komoditas_id', $request->komoditas);
|
||||
}
|
||||
|
||||
if ($request->filled('penghasilan')) {
|
||||
[$min, $max] = match ($request->penghasilan) {
|
||||
'kurang_1' => [null, 999999],
|
||||
'1_3' => [1000000, 3000000],
|
||||
'3_5' => [3000001, 5000000],
|
||||
'5_10' => [5000001, 10000000],
|
||||
'lebih_10' => [10000001, null],
|
||||
default => [null, null],
|
||||
};
|
||||
|
||||
$query->whereIn('id', function ($sub) use ($min, $max) {
|
||||
$sub->from('pendapatan_komoditas')
|
||||
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
||||
->whereNotNull('sosial_ekonomi.petani_id')
|
||||
->groupBy('sosial_ekonomi.petani_id')
|
||||
->select('sosial_ekonomi.petani_id')
|
||||
->when($min !== null, fn ($q) => $q->havingRaw('SUM(pendapatan_komoditas.pendapatan) >= ?', [$min]))
|
||||
->when($max !== null, fn ($q) => $q->havingRaw('SUM(pendapatan_komoditas.pendapatan) <= ?', [$max]));
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('luas_lahan')) {
|
||||
$lahanQuery = Lahan::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->select('petani_id')
|
||||
->distinct();
|
||||
|
||||
match ($request->luas_lahan) {
|
||||
'kurang_05' => $lahanQuery->where('luas_lahan', '<', 0.5),
|
||||
'05_1' => $lahanQuery->whereBetween('luas_lahan', [0.5, 1]),
|
||||
'1_2' => $lahanQuery->where('luas_lahan', '>', 1)->where('luas_lahan', '<=', 2),
|
||||
'lebih_2' => $lahanQuery->where('luas_lahan', '>', 2),
|
||||
default => null,
|
||||
};
|
||||
|
||||
$query->whereIn('petani.id', $lahanQuery);
|
||||
}
|
||||
|
||||
if ($request->filled('status_kepemilikan_lahan')) {
|
||||
$query->whereIn('petani.id', Lahan::query()
|
||||
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||
->where('status_kepemilikan', $request->status_kepemilikan_lahan)
|
||||
->select('petani_id')
|
||||
->distinct());
|
||||
}
|
||||
|
||||
$daftarPetani = $query->orderBy('id')->paginate(10);
|
||||
|
||||
$pendidikanLabels = [
|
||||
'01_tidak_sekolah' => 'Tidak Sekolah',
|
||||
'02_sd' => 'SD',
|
||||
'03_smp' => 'SMP',
|
||||
'04_sma' => 'SMA',
|
||||
'05_diploma' => 'Diploma',
|
||||
'06_sarjana' => 'Sarjana',
|
||||
'07_magister' => 'Magister',
|
||||
];
|
||||
|
||||
$rows = collect($daftarPetani->items())->map(function ($petani) use ($pendidikanLabels) {
|
||||
$umur = $petani->tanggal_lahir ? \Carbon\Carbon::parse($petani->tanggal_lahir)->age : null;
|
||||
$pendidikanRaw = $petani->getRawOriginal('pendidikan_terakhir');
|
||||
$pendapatan = $petani->total_pendapatan;
|
||||
|
||||
$rentangPenghasilan = match (true) {
|
||||
$pendapatan === null => '-',
|
||||
$pendapatan < 1000000 => '< Rp1 juta',
|
||||
$pendapatan <= 3000000 => 'Rp1 - 3 juta',
|
||||
$pendapatan <= 5000000 => 'Rp3 - 5 juta',
|
||||
$pendapatan <= 10000000 => 'Rp5 - 10 juta',
|
||||
default => '> Rp10 juta',
|
||||
};
|
||||
|
||||
return [
|
||||
'id' => $petani->id,
|
||||
'nama' => $petani->nama ?? '-',
|
||||
'jenis_kelamin' => $petani->jenis_kelamin,
|
||||
'gender' => $petani->jenis_kelamin == 1 ? 'Laki-laki' : 'Perempuan',
|
||||
'umur' => $umur,
|
||||
'komoditas' => $petani->komoditas?->nama ?? '-',
|
||||
'luas_lahan' => (float) ($petani->total_luas_lahan ?? 0),
|
||||
'pendidikan' => $pendidikanLabels[$pendidikanRaw] ?? '-',
|
||||
'rentang_penghasilan' => $rentangPenghasilan,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $rows,
|
||||
'pagination' => [
|
||||
'current_page' => $daftarPetani->currentPage(),
|
||||
'last_page' => $daftarPetani->lastPage(),
|
||||
'per_page' => $daftarPetani->perPage(),
|
||||
'total' => $daftarPetani->total(),
|
||||
'from' => $daftarPetani->firstItem(),
|
||||
'to' => $daftarPetani->lastItem(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
|
||||
|
||||
class IndeksPetaniController extends Controller
|
||||
{
|
||||
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
return [
|
||||
'desaKelurahan_id' => $request->desaKelurahan_id,
|
||||
'kecamatan_id' => $request->kecamatan_id,
|
||||
'kabupatenKota_id' => $request->kabupatenKota_id,
|
||||
'provinsi_id' => $request->provinsi_id,
|
||||
'komoditas_id' => $request->komoditas_id,
|
||||
'segmen' => $request->segmen,
|
||||
];
|
||||
}
|
||||
|
||||
private function petaniQuery(array $filters): Builder
|
||||
{
|
||||
$petaniQuery = Petani::query()
|
||||
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
||||
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
||||
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
||||
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
||||
->whereNull('petani.deleted_at')
|
||||
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
|
||||
->when($filters['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
|
||||
->when($filters['kecamatan_id'], fn($q, $id) => $q->where('master_desa_kelurahan.kecamatan_id', $id))
|
||||
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('petani.komoditas_id', $id));
|
||||
|
||||
if ($filters['segmen']) {
|
||||
$petaniQuery->whereHas('skor', fn (Builder $query) => $this->applySegmentFilter($query, $filters['segmen']));
|
||||
}
|
||||
|
||||
return $petaniQuery;
|
||||
}
|
||||
|
||||
private function skorPetaniQuery(array $filters, array $komoditas): Builder
|
||||
{
|
||||
$skor = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
||||
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
||||
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
||||
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
||||
->whereNull('petani.deleted_at')
|
||||
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
|
||||
->when($filters['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
|
||||
->when($filters['kecamatan_id'], fn($q, $id) => $q->where('master_desa_kelurahan.kecamatan_id', $id))
|
||||
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
|
||||
->whereIn('petani.komoditas_id', $komoditas);
|
||||
|
||||
return $this->applySegmentFilter($skor, $filters['segmen']);
|
||||
}
|
||||
|
||||
private function applySegmentFilter(Builder $query, ?string $segment): Builder
|
||||
{
|
||||
$ranges = [
|
||||
'Penggerak' => [4, null],
|
||||
'Perintis' => [3, 4],
|
||||
'Pemerhati' => [2, 3],
|
||||
'Pemula' => [1, 2],
|
||||
];
|
||||
|
||||
if (isset($ranges[$segment])) {
|
||||
[$min, $max] = $ranges[$segment];
|
||||
|
||||
return $query->where('skor_kesiapan_adopsi', '>=', $min)
|
||||
->when($max !== null, fn (Builder $query) => $query->where('skor_kesiapan_adopsi', '<', $max));
|
||||
}
|
||||
|
||||
if ($segment === 'Belum Lengkap') {
|
||||
$query->where(fn (Builder $query) => $query->whereNull('skor_kesiapan_adopsi')
|
||||
->orWhere('skor_kesiapan_adopsi', '<', 1));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return redirect()->route('dashboard.karakteristik-petani', array_merge($request->query(), [
|
||||
'tab' => 'indeks-petani',
|
||||
]));
|
||||
}
|
||||
|
||||
public function contentData(Request $request): array
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
|
||||
$segments = $this->segments();
|
||||
$colors = collect($segments)
|
||||
->pluck('color')
|
||||
->map(fn($c) => "var(--bs-{$c})")
|
||||
->values();
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)->get(['id', 'nama']);
|
||||
$petaniQuery = $this->petaniQuery(array_fill_keys(array_keys($filters), null));
|
||||
|
||||
$desaKelurahan = $petaniQuery->clone()
|
||||
->selectRaw("
|
||||
master_desa_kelurahan.id as desa_kelurahan_id,
|
||||
master_desa_kelurahan.nama as desa_kelurahan,
|
||||
master_kecamatan.id as kecamatan_id,
|
||||
master_kecamatan.nama as kecamatan,
|
||||
master_kabupaten_kota.id as kabupaten_id,
|
||||
master_kabupaten_kota.nama as kabupaten,
|
||||
master_provinsi.id as provinsi_id,
|
||||
master_provinsi.nama as provinsi
|
||||
")
|
||||
->whereNotNull('master_provinsi.id')
|
||||
->distinct()
|
||||
->get();
|
||||
|
||||
$provinsi = $desaKelurahan
|
||||
->unique('provinsi_id')
|
||||
->sortBy('provinsi')
|
||||
->values();
|
||||
|
||||
$wilayah = $desaKelurahan
|
||||
->groupBy('provinsi_id')
|
||||
->map(function ($provinsi) {
|
||||
return [
|
||||
'id' => $provinsi->first()->provinsi_id,
|
||||
'nama' => $provinsi->first()->provinsi,
|
||||
'kabupaten' => $provinsi
|
||||
->groupBy('kabupaten_id')
|
||||
->map(function ($kabupaten) {
|
||||
return [
|
||||
'id' => $kabupaten->first()->kabupaten_id,
|
||||
'nama' => $kabupaten->first()->kabupaten,
|
||||
'kecamatan' => $kabupaten
|
||||
->groupBy('kecamatan_id')
|
||||
->map(function ($kecamatan) {
|
||||
return [
|
||||
'id' => $kecamatan->first()->kecamatan_id,
|
||||
'nama' => $kecamatan->first()->kecamatan,
|
||||
'desa' => $kecamatan->map(function ($desa) {
|
||||
return [
|
||||
'id' => $desa->desa_kelurahan_id,
|
||||
'nama' => $desa->desa_kelurahan,
|
||||
];
|
||||
})->values(),
|
||||
];
|
||||
})->values(),
|
||||
];
|
||||
})->values(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return array_merge($filters,
|
||||
compact(
|
||||
'komoditas',
|
||||
'desaKelurahan',
|
||||
'provinsi',
|
||||
'wilayah',
|
||||
'segments',
|
||||
'colors',
|
||||
));
|
||||
}
|
||||
|
||||
public function summary(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$petaniQuery = $this->petaniQuery($filters);
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$counts = $petaniQuery->clone()
|
||||
->selectRaw('COUNT(*) as petani')
|
||||
->selectRaw('COUNT(DISTINCT petani.komoditas_id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as komoditas')
|
||||
->selectRaw('COUNT(DISTINCT petani.desa_kelurahan_id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as desa')
|
||||
->selectRaw('COUNT(DISTINCT master_kecamatan.id) FILTER (WHERE petani.komoditas_id IN (SELECT id FROM master_komoditas WHERE is_utama = true AND deleted_at IS NULL)) as kecamatan')
|
||||
->first();
|
||||
|
||||
$filteredPetaniIds = $petaniQuery->clone()->select('petani.id');
|
||||
$lahan = Lahan::query()
|
||||
->whereIn('petani_id', $filteredPetaniIds)
|
||||
->whereIn('komoditas_id', $komoditas->pluck('id'))
|
||||
->selectRaw('COALESCE(SUM(luas_lahan), 0) as total, AVG(luas_lahan) as rata_rata')
|
||||
->first();
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
$avgSkor = $skorQuery->clone()->avg('skor_kesiapan_adopsi');
|
||||
|
||||
if ($avgSkor >= 4.0) {
|
||||
$avgSegmen = 'Penggerak';
|
||||
} elseif ($avgSkor >= 3.0) {
|
||||
$avgSegmen = 'Perintis';
|
||||
} elseif ($avgSkor >= 2.0) {
|
||||
$avgSegmen = 'Pemerhati';
|
||||
} elseif ($avgSkor >= 1.0) {
|
||||
$avgSegmen = 'Pemula';
|
||||
} else {
|
||||
$avgSegmen = 'Belum Lengkap';
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'petani' => (int) $counts->petani,
|
||||
'komoditas' => (int) $counts->komoditas,
|
||||
'sum_lahan' => (float) $lahan->total,
|
||||
'avg_lahan' => (float) $lahan->rata_rata,
|
||||
'desa' => (int) $counts->desa,
|
||||
'kecamatan' => (int) $counts->kecamatan,
|
||||
'skor' => $avgSkor,
|
||||
'segmen' => $avgSegmen
|
||||
]);
|
||||
}
|
||||
|
||||
private function segments(): array
|
||||
{
|
||||
$segments = [
|
||||
[
|
||||
'name' => 'Penggerak',
|
||||
'range' => '4.0 - 5.0',
|
||||
'icon' => 'tabler-crown',
|
||||
'color' => 'success',
|
||||
],
|
||||
[
|
||||
'name' => 'Perintis',
|
||||
'range' => '3.0 - 3.9',
|
||||
'icon' => 'tabler-flag',
|
||||
'color' => 'info',
|
||||
],
|
||||
[
|
||||
'name' => 'Pemerhati',
|
||||
'range' => '2.0 - 2.9',
|
||||
'icon' => 'tabler-eye',
|
||||
'color' => 'warning',
|
||||
],
|
||||
[
|
||||
'name' => 'Pemula',
|
||||
'range' => '1.0 - 1.9',
|
||||
'icon' => 'tabler-seedling',
|
||||
'color' => 'danger',
|
||||
],
|
||||
];
|
||||
|
||||
return $segments;
|
||||
}
|
||||
|
||||
private function cases(): string
|
||||
{
|
||||
$case = "
|
||||
CASE
|
||||
WHEN skor_kesiapan_adopsi >= 4.0 THEN 'Penggerak'
|
||||
WHEN skor_kesiapan_adopsi >= 3.0 THEN 'Perintis'
|
||||
WHEN skor_kesiapan_adopsi >= 2.0 THEN 'Pemerhati'
|
||||
WHEN skor_kesiapan_adopsi >= 1.0 THEN 'Pemula'
|
||||
ELSE 'Belum Lengkap'
|
||||
END
|
||||
";
|
||||
|
||||
return $case;
|
||||
}
|
||||
public function segmen(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$segments = $this->segments();
|
||||
$case = $this->cases();
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
|
||||
$countSegmentasi = $skorQuery->clone()
|
||||
->selectRaw("$case AS segmen, COUNT(*) AS total")
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$series = collect($segments)
|
||||
->map(function ($segment) use ($countSegmentasi) {
|
||||
return $countSegmentasi
|
||||
->where('segmen', $segment['name'])
|
||||
->sum('total');
|
||||
})
|
||||
->values();
|
||||
|
||||
$countPetani = $series->sum();
|
||||
|
||||
$persentase = collect($segments)
|
||||
->map(function ($segment) use ($countSegmentasi, $countPetani) {
|
||||
$total = $countSegmentasi
|
||||
->where('segmen', $segment['name'])
|
||||
->sum('total');
|
||||
|
||||
return $countPetani > 0
|
||||
? ($total / $countPetani) * 100
|
||||
: 0;
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'series' => $series,
|
||||
'persentase' => $persentase,
|
||||
]);
|
||||
}
|
||||
|
||||
public function segmenByKomoditas(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$segments = $this->segments();
|
||||
$case = $this->cases();
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
|
||||
$countSegmentasiByKomoditas = $skorQuery->clone()
|
||||
->selectRaw("
|
||||
petani.komoditas_id,
|
||||
$case AS segmen,
|
||||
COUNT(petani.id) AS total
|
||||
")
|
||||
->groupBy('petani.komoditas_id')
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$seriesSegmentasiKomoditas = collect($segments)->map(function ($segment) use ($countSegmentasiByKomoditas, $komoditas) {
|
||||
return [
|
||||
'name' => $segment['name'],
|
||||
'data' => $komoditas->map(function ($item) use ($countSegmentasiByKomoditas, $segment) {
|
||||
|
||||
return optional(
|
||||
$countSegmentasiByKomoditas->first(function ($row) use ($item, $segment) {
|
||||
return $row->komoditas_id == $item->id
|
||||
&& $row->segmen == $segment['name'];
|
||||
})
|
||||
)->total ?? 0;
|
||||
|
||||
})->values(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json([
|
||||
'categories' => $komoditas->pluck('nama')->values(),
|
||||
'series' => $seriesSegmentasiKomoditas,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function dimensi(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
|
||||
$avgDimensi = $skorQuery->clone()
|
||||
->selectRaw("
|
||||
avg(skor_d2_sistem_praktik_budidaya) as skor_d2_sistem_praktik_budidaya,
|
||||
avg(skor_d4_sosial_ekonomi) as skor_d4_sosial_ekonomi,
|
||||
avg(skor_d6_rantai_nilai_akses_pasar) as skor_d6_rantai_nilai_akses_pasar")
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'radarDimensi' => [
|
||||
round($avgDimensi->skor_d2_sistem_praktik_budidaya, 2),
|
||||
round($avgDimensi->skor_d4_sosial_ekonomi, 2),
|
||||
round($avgDimensi->skor_d6_rantai_nilai_akses_pasar, 2),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function komponen(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
|
||||
$avgKomponen = $skorQuery->clone()
|
||||
->selectRaw("avg(skor_komp_1_d4_k3_motivasi_kapasitas_adopsi) as skor_komp_1_d4_k3_motivasi_kapasitas_adopsi,
|
||||
avg(skor_komp_2_d4_k2_ekonomi_rumah_tangga) as skor_komp_2_d4_k2_ekonomi_rumah_tangga,
|
||||
avg(skor_komp_3_d2_praktik_budidaya_gap) as skor_komp_3_d2_praktik_budidaya_gap,
|
||||
avg(skor_komp_4_d4_k4_keterlibatan_kelembagaan) as skor_komp_4_d4_k4_keterlibatan_kelembagaan,
|
||||
avg(skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan) as skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan,
|
||||
avg(skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar) as skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar")
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'radarKomponen' => [
|
||||
round($avgKomponen->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi, 2),
|
||||
round($avgKomponen->skor_komp_2_d4_k2_ekonomi_rumah_tangga, 2),
|
||||
round($avgKomponen->skor_komp_3_d2_praktik_budidaya_gap, 2),
|
||||
round($avgKomponen->skor_komp_4_d4_k4_keterlibatan_kelembagaan, 2),
|
||||
round($avgKomponen->skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan, 2),
|
||||
round($avgKomponen->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar, 2)
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function villageData(array $filters, bool $includePath = true): \Illuminate\Support\Collection
|
||||
{
|
||||
$case = $this->cases();
|
||||
|
||||
$petaniQuery = $this->petaniQuery($filters);
|
||||
|
||||
$desaKelurahan = $petaniQuery->clone()
|
||||
->selectRaw("
|
||||
master_desa_kelurahan.id as desa_kelurahan_id,
|
||||
master_desa_kelurahan.nama as desa_kelurahan,
|
||||
master_kecamatan.id as kecamatan_id,
|
||||
master_kecamatan.nama as kecamatan,
|
||||
master_kabupaten_kota.id as kabupaten_id,
|
||||
master_kabupaten_kota.nama as kabupaten,
|
||||
master_provinsi.id as provinsi_id,
|
||||
master_provinsi.nama as provinsi
|
||||
")
|
||||
->when($includePath, fn (Builder $query) => $query->addSelect('master_desa_kelurahan.path as path'))
|
||||
->distinct()
|
||||
->get();
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||
|
||||
$mapSegmentasi = $skorQuery->clone()
|
||||
->selectRaw("petani.desa_kelurahan_id,
|
||||
$case AS segmen,
|
||||
COUNT(petani.id) AS total
|
||||
")
|
||||
->groupBy('petani.desa_kelurahan_id')
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$mapAvgSkorSegmentasi = $skorQuery->clone()
|
||||
->selectRaw("petani.desa_kelurahan_id,
|
||||
COUNT(petani.id) AS jumlah_petani,
|
||||
avg(skor_kesiapan_adopsi) AS skor,
|
||||
CASE
|
||||
WHEN AVG(skor_kesiapan_adopsi) >= 4 THEN 'Penggerak'
|
||||
WHEN AVG(skor_kesiapan_adopsi) >= 3 THEN 'Perintis'
|
||||
WHEN AVG(skor_kesiapan_adopsi) >= 2 THEN 'Pemerhati'
|
||||
WHEN AVG(skor_kesiapan_adopsi) >= 1 THEN 'Pemula'
|
||||
ELSE 'Belum Lengkap'
|
||||
END AS segmen
|
||||
")
|
||||
->groupBy('petani.desa_kelurahan_id')
|
||||
->get();
|
||||
|
||||
$segmentasiGroups = $mapSegmentasi->groupBy('desa_kelurahan_id');
|
||||
$jumlahSegmenByDesa = $segmentasiGroups
|
||||
->map(fn ($items) => $items->pluck('total', 'segmen'));
|
||||
|
||||
$segmenDominan = $segmentasiGroups
|
||||
->map(function ($items) {
|
||||
return $items->reduce(fn ($largest, $item) => $largest === null || $item->total > $largest->total ? $item : $largest);
|
||||
});
|
||||
|
||||
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan, $jumlahSegmenByDesa) {
|
||||
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
|
||||
|
||||
$item->segmen_dominan = $dominan?->segmen;
|
||||
$item->jumlah_dominan = $dominan?->total;
|
||||
$item->jumlah_segmen = $jumlahSegmenByDesa->get($item->desa_kelurahan_id, collect());
|
||||
});
|
||||
|
||||
$segmentasiByDesa = $mapAvgSkorSegmentasi->keyBy('desa_kelurahan_id');
|
||||
$desaLookup = $desaKelurahan->keyBy('desa_kelurahan_id');
|
||||
|
||||
$mapDesaSegmentasi = $segmentasiByDesa->map(function ($segmentasi, $desaId) use ($desaLookup) {
|
||||
$desa = $desaLookup->get($desaId);
|
||||
if (!$desa) {
|
||||
return null;
|
||||
}
|
||||
$desa->segmentasi = $segmentasi;
|
||||
|
||||
return $desa;
|
||||
})->filter()->values();
|
||||
|
||||
return $mapDesaSegmentasi;
|
||||
}
|
||||
|
||||
public function map(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json($this->villageData($this->filters($request)));
|
||||
}
|
||||
|
||||
public function desa(Request $request): JsonResponse
|
||||
{
|
||||
$segmentStyles = collect($this->segments())->keyBy('name');
|
||||
$mapDesaSegmentasi = $this->villageData($this->filters($request), false);
|
||||
|
||||
$segmentNames = ['Pemula', 'Perintis', 'Pemerhati', 'Penggerak', 'Belum Lengkap'];
|
||||
$rows = $mapDesaSegmentasi->map(function ($desa) use ($segmentStyles, $segmentNames) {
|
||||
$total = (int) $desa->segmentasi->jumlah_petani;
|
||||
|
||||
$style = $segmentStyles[$desa->segmentasi->segmen] ?? ['color' => 'secondary'];
|
||||
$styleDominan = $segmentStyles[$desa->segmentasi->segmen_dominan] ?? ['color' => 'secondary'];
|
||||
|
||||
return [
|
||||
'desa' => $desa->desa_kelurahan,
|
||||
'jumlah_petani' => $desa->segmentasi->jumlah_petani,
|
||||
'segmen' => $desa->segmentasi->segmen,
|
||||
'skor' => $desa->segmentasi->skor,
|
||||
'segmen_dominan' => $desa->segmentasi->segmen_dominan,
|
||||
'jumlah_dominan' => $desa->segmentasi->jumlah_dominan,
|
||||
'color' => $style['color'],
|
||||
'color_dominan' => $styleDominan['color'],
|
||||
'segments' => collect($segmentNames)
|
||||
->mapWithKeys(function ($name) use ($desa, $segmentStyles, $total) {
|
||||
$count = (int) $desa->segmentasi->jumlah_segmen->get($name, 0);
|
||||
|
||||
return [strtolower(str_replace(' ', '_', $name)) => [
|
||||
'count' => $count,
|
||||
'percentage' => $total > 0 ? round($count / $total * 100, 2) : 0,
|
||||
'color' => $segmentStyles[$name]['color'] ?? 'secondary',
|
||||
]];
|
||||
})->all(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($rows);
|
||||
}
|
||||
|
||||
public function data(Request $request): JsonResponse
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$segments = $this->segments();
|
||||
|
||||
$segmentStyles = collect($segments)->keyBy('name');
|
||||
|
||||
$petaniQuery = $this->petaniQuery($filters);
|
||||
|
||||
$komoditas = Komoditas::where('is_utama', true)
|
||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||
|
||||
$daftarPetani = $petaniQuery->clone()
|
||||
->select('petani.*')
|
||||
->with(['desaKelurahan', 'komoditas', 'skor'])
|
||||
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
||||
->whereIn('komoditas_id', $komoditas->pluck('id'))
|
||||
->get();
|
||||
|
||||
$data = $daftarPetani
|
||||
->map(function ($item) use ($segmentStyles) {
|
||||
$segmen = $item->skor
|
||||
? $item->segmen()
|
||||
: null;
|
||||
$style = $segmentStyles[$segmen]
|
||||
?? ['color' => 'secondary'];
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'nama' => $item->nama,
|
||||
'desa' => $item->desaKelurahan?->nama,
|
||||
'desa_kelurahan_id' => $item->desa_kelurahan_id,
|
||||
'komoditas' => $item->komoditas?->nama,
|
||||
'luas_lahan' => (float) ($item->total_luas_lahan ?? 0),
|
||||
'skor' => $item->skor
|
||||
? $item->skor->skor_kesiapan_adopsi
|
||||
: null,
|
||||
'segmen' => $segmen,
|
||||
'color' => $style['color'],
|
||||
'url' => route('dashboard.profil-petani', $item->id),
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Enums\KeparahanSerangan;
|
||||
use App\Enums\PenyakitTanaman;
|
||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||
use App\Enums\SeranganHama;
|
||||
use App\Enums\SosialEkonomi\StatusKepemilikanLahanSosialEkonomi;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\PraktikBudidaya;
|
||||
use App\Models\Pertanian\Produksi;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
use App\Models\Pertanian\SosialEkonomi;
|
||||
use App\Models\Pertanian\SurveyProduktivitas;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use App\Models\Wilayah\KabupatenKota;
|
||||
use App\Models\Wilayah\Kecamatan;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class IndikatorPertanianController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$kabupatenOptions = $this->kabupatenOptions();
|
||||
$ngadaOption = collect($kabupatenOptions)->first(
|
||||
fn ($option) => str_contains(strtolower($option['label']), 'ngada')
|
||||
);
|
||||
$initialFilters = $this->selectedFilters($request, $ngadaOption['value'] ?? 'all');
|
||||
|
||||
$totalPetani = $this->applyPetaniFilters(Petani::query(), $initialFilters)->count();
|
||||
$totalLuasLahan = ((float) $this->applyLahanFilters(Lahan::query(), $initialFilters)->sum('luas_lahan')) / 10000;
|
||||
$rataRataLuasLahan = $totalPetani > 0
|
||||
? $totalLuasLahan / $totalPetani
|
||||
: 0;
|
||||
$totalProduksiKg = $this->totalProduksiKg($initialFilters);
|
||||
$rataRataProduksiKg = $totalPetani > 0
|
||||
? $totalProduksiKg / $totalPetani
|
||||
: 0;
|
||||
$rataRataProduktivitas = $totalLuasLahan > 0
|
||||
? $totalProduksiKg / $totalLuasLahan
|
||||
: 0;
|
||||
|
||||
$filterOptions = [
|
||||
[
|
||||
'key' => 'kabupaten',
|
||||
'label' => 'Kabupaten',
|
||||
'options' => $kabupatenOptions,
|
||||
'icon' => 'tabler-map-pin',
|
||||
'readonly' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'kecamatan',
|
||||
'label' => 'Kecamatan',
|
||||
'options' => $this->kecamatanOptions(),
|
||||
],
|
||||
[
|
||||
'key' => 'desa',
|
||||
'label' => 'Desa',
|
||||
'options' => $this->desaOptions(),
|
||||
],
|
||||
[
|
||||
'key' => 'komoditas',
|
||||
'label' => 'Komoditas',
|
||||
'options' => $this->komoditasOptions(),
|
||||
],
|
||||
[
|
||||
'key' => 'segmentasi',
|
||||
'label' => 'Segmentasi',
|
||||
'options' => $this->segmentasiOptions(),
|
||||
],
|
||||
];
|
||||
|
||||
$summaryData = [
|
||||
[
|
||||
'value' => number_format($totalPetani, 0, ',', '.'),
|
||||
'label' => 'Petani Terdata',
|
||||
'description' => 'Total petani yang masuk sistem',
|
||||
'icon' => 'tabler-users',
|
||||
'variant' => 'primary',
|
||||
],
|
||||
[
|
||||
'value' => number_format($totalLuasLahan, 2, ',', '.').' ha',
|
||||
'label' => 'Total Luas Lahan',
|
||||
'description' => 'Rata-rata '.number_format($rataRataLuasLahan, 2, ',', '.').' ha/petani',
|
||||
'icon' => 'tabler-mountain',
|
||||
'variant' => 'success',
|
||||
],
|
||||
[
|
||||
'value' => number_format($totalProduksiKg, 0, ',', '.').' kg',
|
||||
'label' => 'Total Produksi (Semua Komoditas)',
|
||||
'description' => 'Rata-rata '.number_format($rataRataProduksiKg, 0, ',', '.').' kg/petani',
|
||||
'icon' => 'tabler-wheat',
|
||||
'variant' => 'warning',
|
||||
],
|
||||
[
|
||||
'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha',
|
||||
'label' => 'Rata-rata Produktivitas',
|
||||
'description' => 'Semua komoditas berdasarkan total produksi dan luas lahan',
|
||||
'icon' => 'tabler-chart-bar',
|
||||
'variant' => 'info',
|
||||
],
|
||||
];
|
||||
|
||||
return view('dashboard.indikator-pertanian', [
|
||||
'title' => 'Indikator Pertanian',
|
||||
'initialFilters' => $initialFilters,
|
||||
'filterOptions' => $filterOptions,
|
||||
'summaryData' => $summaryData,
|
||||
'chartData' => [
|
||||
'petaniPerKomoditas' => $this->petaniPerKomoditasChart($initialFilters),
|
||||
'luasLahanPerKecamatan' => $this->luasLahanPerKecamatanChart($initialFilters),
|
||||
'kondisiTanaman' => $this->kondisiTanamanChart($initialFilters),
|
||||
'statusKepemilikanLahan' => $this->statusKepemilikanLahanChart($initialFilters),
|
||||
'seranganHama' => $this->seranganHamaChart($initialFilters),
|
||||
'penyakitTanaman' => $this->penyakitTanamanChart($initialFilters),
|
||||
'keparahanSerangan' => $this->keparahanSeranganChart($initialFilters),
|
||||
'statusHakKelolaLahan' => $this->statusHakKelolaLahanChart($initialFilters),
|
||||
'praktikPemupukan' => $this->praktikPemupukanChart($initialFilters),
|
||||
'produksiPerKomoditas' => $this->produksiPerKomoditasChart($initialFilters),
|
||||
'produktivitasPerKomoditas' => $this->produktivitasPerKomoditasChart($initialFilters),
|
||||
'luasPanenPerKomoditas' => $this->luasPanenPerKomoditasChart($initialFilters),
|
||||
'statusPanen' => $this->statusPanenChart($initialFilters),
|
||||
'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($initialFilters, 'ha_tahun'),
|
||||
'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($initialFilters, 'pohon_tahun'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function selectedFilters(Request $request, string $defaultKabupaten): array
|
||||
{
|
||||
$filters = [
|
||||
'kabupaten' => $defaultKabupaten,
|
||||
'kecamatan' => $this->validFilterValue($request->query('kecamatan'), Kecamatan::class),
|
||||
'desa' => $this->validFilterValue($request->query('desa'), DesaKelurahan::class),
|
||||
'komoditas' => $this->validFilterValue($request->query('komoditas'), Komoditas::class),
|
||||
'segmentasi' => $this->validSegmentasi((string) $request->query('segmentasi', 'all')),
|
||||
];
|
||||
|
||||
if ($filters['desa'] !== 'all') {
|
||||
$desa = DesaKelurahan::query()->find($filters['desa']);
|
||||
|
||||
if (! $desa || ($filters['kecamatan'] !== 'all' && (string) $desa->kecamatan_id !== $filters['kecamatan'])) {
|
||||
$filters['desa'] = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
if ($filters['kecamatan'] === 'all') {
|
||||
$filters['desa'] = 'all';
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function validFilterValue(mixed $value, string $modelClass): string
|
||||
{
|
||||
if (! $value || $value === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
return $modelClass::query()->whereKey($value)->exists()
|
||||
? (string) $value
|
||||
: 'all';
|
||||
}
|
||||
|
||||
private function validSegmentasi(string $value): string
|
||||
{
|
||||
return in_array($value, [
|
||||
'Penggerak',
|
||||
'Perintis',
|
||||
'Pemerhati',
|
||||
'Pemula',
|
||||
'Belum tersegmentasi',
|
||||
'all',
|
||||
], true) ? $value : 'all';
|
||||
}
|
||||
|
||||
private function kabupatenOptions(): array
|
||||
{
|
||||
return $this->withAllOption(
|
||||
'Semua Kabupaten',
|
||||
KabupatenKota::query()
|
||||
->orderBy('nama')
|
||||
->get(['id', 'kode', 'nama'])
|
||||
->map(fn (KabupatenKota $kabupaten) => [
|
||||
'value' => (string) $kabupaten->id,
|
||||
'label' => $this->kabupatenLabel($kabupaten),
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function kecamatanOptions(): array
|
||||
{
|
||||
return $this->withAllOption(
|
||||
'Semua Kecamatan',
|
||||
Kecamatan::query()
|
||||
->orderBy('nama')
|
||||
->get(['id', 'nama'])
|
||||
->map(fn (Kecamatan $kecamatan) => [
|
||||
'value' => (string) $kecamatan->id,
|
||||
'label' => $kecamatan->nama,
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function desaOptions(): array
|
||||
{
|
||||
return $this->withAllOption(
|
||||
'Semua Desa',
|
||||
DesaKelurahan::query()
|
||||
->orderBy('nama')
|
||||
->get(['id', 'nama', 'kecamatan_id'])
|
||||
->map(fn (DesaKelurahan $desa) => [
|
||||
'value' => (string) $desa->id,
|
||||
'label' => $desa->nama,
|
||||
'parent' => (string) $desa->kecamatan_id,
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function komoditasOptions(): array
|
||||
{
|
||||
return $this->withAllOption(
|
||||
'Semua Komoditas',
|
||||
Komoditas::query()
|
||||
->orderBy('nama')
|
||||
->get(['id', 'nama'])
|
||||
->map(fn (Komoditas $komoditas) => [
|
||||
'value' => (string) $komoditas->id,
|
||||
'label' => $komoditas->nama,
|
||||
])
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function segmentasiOptions(): array
|
||||
{
|
||||
// Aggregate bucket counts in SQL instead of pulling every skor_kesiapan_adopsi
|
||||
// value into PHP just to map/filter/unique it.
|
||||
$row = SkorPetani::query()->selectRaw("
|
||||
COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 4) as penggerak,
|
||||
COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 3 AND skor_kesiapan_adopsi < 4) as perintis,
|
||||
COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 2 AND skor_kesiapan_adopsi < 3) as pemerhati,
|
||||
COUNT(*) FILTER (WHERE skor_kesiapan_adopsi >= 1 AND skor_kesiapan_adopsi < 2) as pemula,
|
||||
COUNT(*) FILTER (WHERE skor_kesiapan_adopsi IS NOT NULL AND skor_kesiapan_adopsi < 1) as belum_tersegmentasi
|
||||
")->first();
|
||||
|
||||
$order = [
|
||||
'Penggerak' => $row->penggerak,
|
||||
'Perintis' => $row->perintis,
|
||||
'Pemerhati' => $row->pemerhati,
|
||||
'Pemula' => $row->pemula,
|
||||
'Belum tersegmentasi' => $row->belum_tersegmentasi,
|
||||
];
|
||||
|
||||
$segments = collect($order)
|
||||
->filter(fn ($count) => $count > 0)
|
||||
->keys()
|
||||
->map(fn ($segment) => [
|
||||
'value' => $segment,
|
||||
'label' => $segment,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $this->withAllOption('Semua Segmentasi', $segments);
|
||||
}
|
||||
|
||||
private function totalProduksiKg(array $filters): float
|
||||
{
|
||||
$expr = $this->kgCaseExpr('jumlah_panen', 'satuan');
|
||||
|
||||
return (float) ($this->applyProduksiFilters(Produksi::query(), $filters)
|
||||
->whereNotNull('jumlah_panen')
|
||||
->selectRaw("SUM({$expr}) as total")
|
||||
->value('total') ?? 0);
|
||||
}
|
||||
|
||||
private function petaniPerKomoditasChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyPetaniFilters(Petani::query(), $filters)
|
||||
->leftJoin('master_komoditas as k', 'k.id', '=', 'petani.komoditas_id')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('COUNT(petani.id) as total')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function luasLahanPerKecamatanChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||
->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id')
|
||||
->leftJoin('master_kecamatan as k', 'k.id', '=', 'd.kecamatan_id')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('SUM(map_lahan.luas_lahan) / 10000 as total')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->havingRaw('SUM(map_lahan.luas_lahan) > 0')
|
||||
->orderByDesc('total')
|
||||
->limit(8)
|
||||
->get()
|
||||
->filter(fn ($row) => round((float) $row->total, 2) > 0)
|
||||
->values();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function kondisiTanamanChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
|
||||
->whereNull('map_tanaman.deleted_at')
|
||||
->whereNotNull('map_tanaman.kondisi_tanaman')
|
||||
->selectRaw('map_tanaman.kondisi_tanaman as label')
|
||||
->selectRaw('COUNT(map_tanaman.id) as total')
|
||||
->groupBy('map_tanaman.kondisi_tanaman')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('kondisi_tanaman')
|
||||
->selectRaw('kondisi_tanaman as label')
|
||||
->selectRaw('COUNT(map_lahan.id) as total')
|
||||
->groupBy('kondisi_tanaman')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->map(fn ($label) => $this->shortEnumLabel((string) $label))->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function seranganHamaChart(array $filters): array
|
||||
{
|
||||
return $this->tanamanAttributeChart(
|
||||
$filters,
|
||||
'serangan_hama',
|
||||
fn ($label) => $this->enumLabel(SeranganHama::class, (string) $label)
|
||||
);
|
||||
}
|
||||
|
||||
private function penyakitTanamanChart(array $filters): array
|
||||
{
|
||||
return $this->tanamanAttributeChart(
|
||||
$filters,
|
||||
'penyakit_tanaman',
|
||||
fn ($label) => $this->enumLabel(PenyakitTanaman::class, (string) $label)
|
||||
);
|
||||
}
|
||||
|
||||
private function keparahanSeranganChart(array $filters): array
|
||||
{
|
||||
return $this->tanamanAttributeChart(
|
||||
$filters,
|
||||
'keparahan_serangan',
|
||||
fn ($label) => $this->keparahanSeranganLabel((string) $label)
|
||||
);
|
||||
}
|
||||
|
||||
private function statusKepemilikanLahanChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||
->whereNotNull('status_kepemilikan')
|
||||
->selectRaw('status_kepemilikan as label')
|
||||
->selectRaw('COUNT(map_lahan.id) as total')
|
||||
->groupBy('status_kepemilikan')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return $this->chartFromRows($rows, fn ($label) => $this->shortEnumLabel((string) $label));
|
||||
}
|
||||
|
||||
private function statusHakKelolaLahanChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applySosialEkonomiFilters(SosialEkonomi::query(), $filters)
|
||||
->whereNotNull('status_kepemilikan_lahan')
|
||||
->selectRaw('status_kepemilikan_lahan as label')
|
||||
->selectRaw('COUNT(sosial_ekonomi.id) as total')
|
||||
->groupBy('status_kepemilikan_lahan')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return $this->chartFromRows(
|
||||
$rows,
|
||||
fn ($label) => $this->enumLabel(StatusKepemilikanLahanSosialEkonomi::class, (string) $label)
|
||||
);
|
||||
}
|
||||
|
||||
private function praktikPemupukanChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||
->whereNotNull('pemberian_pupuk')
|
||||
->selectRaw('pemberian_pupuk as label')
|
||||
->selectRaw('COUNT(praktik_budidaya.id) as total')
|
||||
->groupBy('pemberian_pupuk')
|
||||
->orderBy('pemberian_pupuk')
|
||||
->get();
|
||||
|
||||
return $this->chartFromRows(
|
||||
$rows,
|
||||
fn ($label) => $this->pemberianPupukLabel((int) $label)
|
||||
);
|
||||
}
|
||||
|
||||
private function produksiPerKomoditasChart(array $filters): array
|
||||
{
|
||||
$expr = $this->kgCaseExpr('produksi.jumlah_panen', 'produksi.satuan');
|
||||
|
||||
$rows = $this->applyProduksiFilters(Produksi::query(), $filters)
|
||||
->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id')
|
||||
->whereNotNull('jumlah_panen')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw("SUM({$expr}) as total")
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->get()
|
||||
->filter(fn ($row) => round((float) $row->total, 2) > 0)
|
||||
->sortByDesc('total')
|
||||
->values();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function produktivitasPerKomoditasChart(array $filters): array
|
||||
{
|
||||
$expr = $this->kgCaseExpr('produksi.jumlah_panen', 'produksi.satuan');
|
||||
|
||||
$rows = $this->applyProduksiFilters(Produksi::query(), $filters)
|
||||
->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('AVG(produksi.produktivitas) FILTER (WHERE produksi.produktivitas > 0) as avg_prod')
|
||||
->selectRaw("SUM({$expr}) as total_kg")
|
||||
->selectRaw('SUM(produksi.luas_panen) as total_luas')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->get()
|
||||
->map(function ($row) {
|
||||
$avgProd = (float) $row->avg_prod;
|
||||
$totalLuas = (float) $row->total_luas;
|
||||
$totalKg = (float) $row->total_kg;
|
||||
|
||||
$value = $avgProd > 0
|
||||
? $avgProd
|
||||
: ($totalLuas > 0 ? $totalKg / $totalLuas : 0);
|
||||
|
||||
return (object) ['label' => $row->label, 'total' => round($value, 2)];
|
||||
})
|
||||
->filter(fn ($row) => $row->total > 0)
|
||||
->sortByDesc('total')
|
||||
->values();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function luasPanenPerKomoditasChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyProduksiFilters(Produksi::query(), $filters)
|
||||
->leftJoin('master_komoditas as k', 'k.id', '=', 'produksi.komoditas_id')
|
||||
->whereNotNull('luas_panen')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('SUM(produksi.luas_panen) as total')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->havingRaw('SUM(produksi.luas_panen) > 0')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function statusPanenChart(array $filters): array
|
||||
{
|
||||
$rows = $this->applyProduksiFilters(Produksi::query(), $filters)
|
||||
->whereNotNull('status_panen')
|
||||
->selectRaw("COALESCE(status_panen, 'Belum ditentukan') as label")
|
||||
->selectRaw('COUNT(produksi.id) as total')
|
||||
->groupBy('status_panen')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return $this->chartFromRows($rows, fn ($label) => $this->shortEnumLabel((string) $label));
|
||||
}
|
||||
|
||||
private function surveyProduktivitasByUnitChart(array $filters, string $unit): array
|
||||
{
|
||||
$rows = $this->applySurveyProduktivitasFilters(SurveyProduktivitas::query(), $filters)
|
||||
->leftJoin('master_komoditas as k', 'k.id', '=', 'survey_produktivitas.komoditas_id')
|
||||
->where('satuan', $unit)
|
||||
->whereNotNull('kuantitas')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('AVG(survey_produktivitas.kuantitas) as total')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->havingRaw('AVG(survey_produktivitas.kuantitas) > 0')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'labels' => $rows->pluck('label')->values()->all(),
|
||||
'series' => $rows->pluck('total')->map(fn ($value) => round((float) $value, 2))->values()->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function chartFromRows($rows, callable $labelResolver): array
|
||||
{
|
||||
return [
|
||||
'labels' => $rows
|
||||
->pluck('label')
|
||||
->map(fn ($label) => $labelResolver($label))
|
||||
->values()
|
||||
->all(),
|
||||
'series' => $rows
|
||||
->pluck('total')
|
||||
->map(fn ($value) => (int) $value)
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
private function tanamanAttributeChart(array $filters, string $field, callable $labelResolver): array
|
||||
{
|
||||
$qualifiedField = 'map_tanaman.'.$field;
|
||||
|
||||
$rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
|
||||
->whereNull('map_tanaman.deleted_at')
|
||||
->whereNotNull($qualifiedField)
|
||||
->selectRaw($qualifiedField.' as label')
|
||||
->selectRaw('COUNT(map_tanaman.id) as total')
|
||||
->groupBy($qualifiedField)
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||
->whereNotNull($field)
|
||||
->selectRaw($field.' as label')
|
||||
->selectRaw('COUNT(map_lahan.id) as total')
|
||||
->groupBy($field)
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
}
|
||||
|
||||
return $this->chartFromRows($rows, $labelResolver);
|
||||
}
|
||||
|
||||
private function applyPetaniFilters($query, array $filters)
|
||||
{
|
||||
return $query
|
||||
->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) {
|
||||
$query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan']));
|
||||
})
|
||||
->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa']))
|
||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']))
|
||||
->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) {
|
||||
$query->whereHas('skor', function ($skor) use ($filters) {
|
||||
$this->applySegmentasiRange($skor, $filters['segmentasi']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function applyLahanFilters($query, array $filters)
|
||||
{
|
||||
return $query
|
||||
->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) {
|
||||
$query->whereHas('desaKelurahan', fn ($desa) => $desa->where('kecamatan_id', $filters['kecamatan']));
|
||||
})
|
||||
->when($filters['desa'] !== 'all', fn ($query) => $query->where('desa_kelurahan_id', $filters['desa']))
|
||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']))
|
||||
->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) {
|
||||
$query->whereIn('petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi']));
|
||||
});
|
||||
}
|
||||
|
||||
private function applyProduksiFilters($query, array $filters)
|
||||
{
|
||||
return $query
|
||||
->when($filters['kecamatan'] !== 'all' || $filters['desa'] !== 'all' || $filters['segmentasi'] !== 'all', function ($query) use ($filters) {
|
||||
$query->whereHas('lahan', function ($lahan) use ($filters) {
|
||||
$this->applyLahanFilters($lahan, $filters);
|
||||
});
|
||||
})
|
||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']));
|
||||
}
|
||||
|
||||
private function applySosialEkonomiFilters($query, array $filters)
|
||||
{
|
||||
return $query->whereHas('petani', function ($petani) use ($filters) {
|
||||
$this->applyPetaniFilters($petani, $filters);
|
||||
});
|
||||
}
|
||||
|
||||
private function applyPraktikBudidayaFilters($query, array $filters)
|
||||
{
|
||||
return $query->whereHas('petani', function ($petani) use ($filters) {
|
||||
$this->applyPetaniFilters($petani, $filters);
|
||||
});
|
||||
}
|
||||
|
||||
private function applySurveyProduktivitasFilters($query, array $filters)
|
||||
{
|
||||
return $query
|
||||
->whereHas('petani', function ($petani) use ($filters) {
|
||||
$this->applyPetaniFilters($petani, $filters);
|
||||
})
|
||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']));
|
||||
}
|
||||
|
||||
private function applyTanamanTableFilters($query, array $filters)
|
||||
{
|
||||
return $query
|
||||
->leftJoin('map_lahan as l', 'l.id', '=', 'map_tanaman.lahan_id')
|
||||
->when($filters['kecamatan'] !== 'all', function ($query) use ($filters) {
|
||||
$query
|
||||
->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'l.desa_kelurahan_id')
|
||||
->where('d.kecamatan_id', $filters['kecamatan']);
|
||||
})
|
||||
->when($filters['desa'] !== 'all', fn ($query) => $query->where('l.desa_kelurahan_id', $filters['desa']))
|
||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('map_tanaman.komoditas_id', $filters['komoditas']))
|
||||
->when($filters['segmentasi'] !== 'all', function ($query) use ($filters) {
|
||||
$query->where(function ($query) use ($filters) {
|
||||
$query
|
||||
->whereIn('map_tanaman.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi']))
|
||||
->orWhereIn('l.petani_id', $this->segmentasiPetaniIdsQuery($filters['segmentasi']));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function segmentasiPetaniIdsQuery(string $segmentasi)
|
||||
{
|
||||
return Petani::query()
|
||||
->select('id')
|
||||
->whereHas('skor', function ($skor) use ($segmentasi) {
|
||||
$this->applySegmentasiRange($skor, $segmentasi);
|
||||
});
|
||||
}
|
||||
|
||||
private function applySegmentasiRange($query, string $segmentasi)
|
||||
{
|
||||
return match ($segmentasi) {
|
||||
'Penggerak' => $query->where('skor_kesiapan_adopsi', '>=', 4),
|
||||
'Perintis' => $query->where('skor_kesiapan_adopsi', '>=', 3)->where('skor_kesiapan_adopsi', '<', 4),
|
||||
'Pemerhati' => $query->where('skor_kesiapan_adopsi', '>=', 2)->where('skor_kesiapan_adopsi', '<', 3),
|
||||
'Pemula' => $query->where('skor_kesiapan_adopsi', '>=', 1)->where('skor_kesiapan_adopsi', '<', 2),
|
||||
default => $query->where(function ($query) {
|
||||
$query->whereNull('skor_kesiapan_adopsi')->orWhere('skor_kesiapan_adopsi', '<', 1);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL CASE expression converting a jumlah/satuan pair to kilograms,
|
||||
* so aggregation happens in the database instead of pulling every
|
||||
* row of `produksi` into PHP to sum with toKilograms().
|
||||
*/
|
||||
private function kgCaseExpr(string $jumlahColumn, string $satuanColumn): string
|
||||
{
|
||||
return "CASE LOWER({$satuanColumn})
|
||||
WHEN 'ton' THEN {$jumlahColumn} * 1000
|
||||
WHEN 'kuintal' THEN {$jumlahColumn} * 100
|
||||
WHEN 'kwintal' THEN {$jumlahColumn} * 100
|
||||
ELSE {$jumlahColumn}
|
||||
END";
|
||||
}
|
||||
|
||||
private function shortEnumLabel(string $value): string
|
||||
{
|
||||
return str($value)
|
||||
->replace('_', ' ')
|
||||
->headline()
|
||||
->toString();
|
||||
}
|
||||
|
||||
private function enumLabel(string $enumClass, int|string $value): string
|
||||
{
|
||||
$case = $enumClass::tryFrom($value);
|
||||
|
||||
if (! $case) {
|
||||
return $this->shortEnumLabel((string) $value);
|
||||
}
|
||||
|
||||
return method_exists($case, 'label')
|
||||
? $case->label()
|
||||
: $this->shortEnumLabel((string) $case->value);
|
||||
}
|
||||
|
||||
private function keparahanSeranganLabel(string $value): string
|
||||
{
|
||||
return match (KeparahanSerangan::tryFrom($value)) {
|
||||
KeparahanSerangan::RINGAN => 'Ringan',
|
||||
KeparahanSerangan::SEDANG => 'Sedang',
|
||||
KeparahanSerangan::BERAT => 'Parah',
|
||||
default => $this->shortEnumLabel($value),
|
||||
};
|
||||
}
|
||||
|
||||
private function pemberianPupukLabel(int $value): string
|
||||
{
|
||||
return match (PemberianPupuk::tryFrom($value)) {
|
||||
PemberianPupuk::TIDAK_PERNAH => 'Tidak pernah',
|
||||
PemberianPupuk::JENIS_DOSIS_WAKTU_TIDAK_MENENTU => 'Tidak menentu',
|
||||
PemberianPupuk::JENIS_SESUAI_DOSIS_TIDAK_MENENTU => 'Jenis sesuai',
|
||||
PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_CUKUP_TEPAT => 'Cukup tepat',
|
||||
PemberianPupuk::JENIS_DOSIS_SESUAI_WAKTU_TEPAT => 'Tepat & rutin',
|
||||
default => $this->shortEnumLabel((string) $value),
|
||||
};
|
||||
}
|
||||
|
||||
private function toKilograms(float $value, string $unit): float
|
||||
{
|
||||
return match (strtolower($unit)) {
|
||||
'ton' => $value * 1000,
|
||||
'kuintal', 'kwintal' => $value * 100,
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
private function withAllOption(string $label, array $options): array
|
||||
{
|
||||
return array_merge([
|
||||
[
|
||||
'value' => 'all',
|
||||
'label' => $label,
|
||||
],
|
||||
], $options);
|
||||
}
|
||||
|
||||
private function kabupatenLabel(KabupatenKota $kabupaten): string
|
||||
{
|
||||
return preg_replace('/^(kabupaten|kota)\s+/i', '', $kabupaten->nama);
|
||||
}
|
||||
|
||||
private function segmentLabel(float $score): string
|
||||
{
|
||||
return match (true) {
|
||||
$score >= 4.0 => 'Penggerak',
|
||||
$score >= 3.0 => 'Perintis',
|
||||
$score >= 2.0 => 'Pemerhati',
|
||||
$score >= 1.0 => 'Pemula',
|
||||
default => 'Belum tersegmentasi',
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,15 +17,18 @@ class ProfilPetaniController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
if (!Gate::allows('super_admin') && !Gate::allows('admin')) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
$title = 'Dashboard Profil Petani';
|
||||
|
||||
$komoditas = Komoditas::all();
|
||||
$komoditas = Komoditas::select('id', 'nama')->where('is_utama', true)->get();
|
||||
$komoditasIds = $komoditas->pluck('id');
|
||||
|
||||
$petani = Petani::pluck('id');
|
||||
$lahan = Lahan::whereIn('petani_id', $petani)->get();
|
||||
$petaniIds = fn () => Petani::query()->select('id');
|
||||
$lahanIds = fn () => Lahan::query()
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->select('id');
|
||||
|
||||
$petaniSummary = Petani::select(
|
||||
'komoditas_id',
|
||||
@@ -35,17 +38,17 @@ class ProfilPetaniController extends Controller
|
||||
->get();
|
||||
$lahanSummary = Lahan::select(
|
||||
'komoditas_id',
|
||||
DB::raw('COUNT(*) as total',
|
||||
DB::raw('SUM(luas_lahan) as luas_lahan'))
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw('SUM(luas_lahan) as luas_lahan')
|
||||
)
|
||||
->whereIn('petani_id', $petani)
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->groupBy('komoditas_id')
|
||||
->get();
|
||||
$tanamanSummary = Tanaman::select(
|
||||
'komoditas_id',
|
||||
DB::raw('COUNT(*) as total')
|
||||
)
|
||||
->whereIn('lahan_id', $lahan->pluck('id'))
|
||||
->whereIn('lahan_id', $lahanIds())
|
||||
->groupBy('komoditas_id')
|
||||
->get();
|
||||
|
||||
@@ -59,7 +62,7 @@ class ProfilPetaniController extends Controller
|
||||
END
|
||||
";
|
||||
$skorSummary = SkorPetani::selectRaw("$case AS segmen, COUNT(*) AS total")
|
||||
->whereIn('petani_id', $petani)
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
@@ -70,10 +73,86 @@ class ProfilPetaniController extends Controller
|
||||
$case AS segmen,
|
||||
COUNT(petani.id) AS total
|
||||
")
|
||||
->whereIn('petani_skor.petani_id', $petani)
|
||||
->whereIn('petani_skor.petani_id', $petaniIds())
|
||||
->groupBy('petani.komoditas_id')
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$skorAverage = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->whereIn('petani.komoditas_id', $komoditasIds)
|
||||
->avg('skor_kesiapan_adopsi');
|
||||
if ($skorAverage >= 4.0) {
|
||||
$segmenAverage = 'Penggerak';
|
||||
} elseif ($skorAverage >= 3.0) {
|
||||
$segmenAverage = 'Perintis';
|
||||
} elseif ($skorAverage >= 2.0) {
|
||||
$segmenAverage = 'Pemerhati';
|
||||
} elseif ($skorAverage >= 1.0) {
|
||||
$segmenAverage = 'Pemula';
|
||||
} else {
|
||||
$segmenAverage = 'Belum Lengkap';
|
||||
}
|
||||
|
||||
$skorDimensi = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->whereIn('petani.komoditas_id', $komoditasIds)
|
||||
->selectRaw("
|
||||
avg(skor_d2_sistem_praktik_budidaya) as skor_d2_sistem_praktik_budidaya,
|
||||
avg(skor_d4_sosial_ekonomi) as skor_d4_sosial_ekonomi,
|
||||
avg(skor_d6_rantai_nilai_akses_pasar) as skor_d6_rantai_nilai_akses_pasar")
|
||||
->first();
|
||||
|
||||
$skorKomponen = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->whereIn('petani.komoditas_id', $komoditasIds)
|
||||
->selectRaw("avg(skor_komp_1_d4_k3_motivasi_kapasitas_adopsi) as skor_komp_1_d4_k3_motivasi_kapasitas_adopsi,
|
||||
avg(skor_komp_2_d4_k2_ekonomi_rumah_tangga) as skor_komp_2_d4_k2_ekonomi_rumah_tangga,
|
||||
avg(skor_komp_3_d2_praktik_budidaya_gap) as skor_komp_3_d2_praktik_budidaya_gap,
|
||||
avg(skor_komp_4_d4_k4_keterlibatan_kelembagaan) as skor_komp_4_d4_k4_keterlibatan_kelembagaan,
|
||||
avg(skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan) as skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan,
|
||||
avg(skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar) as skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar")
|
||||
->first();
|
||||
|
||||
$skorMultiDimensi = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->whereIn('petani.komoditas_id', $komoditasIds)
|
||||
->selectRaw("$case AS segmen,
|
||||
avg(skor_kesiapan_adopsi) as skor_kesiapan_adopsi,
|
||||
avg(skor_d2_sistem_praktik_budidaya) as skor_d2_sistem_praktik_budidaya,
|
||||
avg(skor_d4_sosial_ekonomi) as skor_d4_sosial_ekonomi,
|
||||
avg(skor_d6_rantai_nilai_akses_pasar) as skor_d6_rantai_nilai_akses_pasar")
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$skorMultiKomponen = SkorPetani::query()
|
||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||
->whereIn('petani_id', $petaniIds())
|
||||
->whereIn('petani.komoditas_id', $komoditasIds)
|
||||
->selectRaw("$case AS segmen,
|
||||
avg(skor_kesiapan_adopsi) as skor_kesiapan_adopsi,
|
||||
avg(skor_komp_1_d4_k3_motivasi_kapasitas_adopsi) as skor_komp_1_d4_k3_motivasi_kapasitas_adopsi,
|
||||
avg(skor_komp_2_d4_k2_ekonomi_rumah_tangga) as skor_komp_2_d4_k2_ekonomi_rumah_tangga,
|
||||
avg(skor_komp_3_d2_praktik_budidaya_gap) as skor_komp_3_d2_praktik_budidaya_gap,
|
||||
avg(skor_komp_4_d4_k4_keterlibatan_kelembagaan) as skor_komp_4_d4_k4_keterlibatan_kelembagaan,
|
||||
avg(skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan) as skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan,
|
||||
avg(skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar) as skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar")
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
$petani = Petani::query()
|
||||
->select('id', 'nama', 'jenis_kelamin', 'tanggal_lahir', 'komoditas_id')
|
||||
->with([
|
||||
'komoditas:id,nama',
|
||||
'skor:id,petani_id,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',
|
||||
])
|
||||
->whereIn('komoditas_id', $komoditasIds)
|
||||
->orderBy('nama')
|
||||
->get();
|
||||
|
||||
return view('dashboard.petani', compact('title',
|
||||
'komoditas',
|
||||
@@ -81,7 +160,14 @@ class ProfilPetaniController extends Controller
|
||||
'lahanSummary',
|
||||
'tanamanSummary',
|
||||
'skorSummary',
|
||||
'skorKomoditasSummary'
|
||||
'skorKomoditasSummary',
|
||||
'segmenAverage',
|
||||
'skorAverage',
|
||||
'skorDimensi',
|
||||
'skorKomponen',
|
||||
'skorMultiDimensi',
|
||||
'skorMultiKomponen',
|
||||
'petani',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Web\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -11,14 +12,18 @@ use App\Models\User;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\Tanaman;
|
||||
use App\Models\Pertanian\SurveyPelatihan;
|
||||
use App\Models\Pertanian\SurveySertifikasi;
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Enums\AksesPasar\SaluranTerdahulu;
|
||||
use App\Enums\AksesPasar\SkorPengalaman;
|
||||
|
||||
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();
|
||||
@@ -26,7 +31,7 @@ class SurveyPetaniController extends Controller
|
||||
// Progres per enumerator
|
||||
|
||||
$enumerators = User::query()
|
||||
->where('role', 'fasilitator')
|
||||
->where('role', 'enumerator')
|
||||
->withCount([
|
||||
'petaniDibuat as done',
|
||||
])
|
||||
@@ -57,11 +62,13 @@ class SurveyPetaniController extends Controller
|
||||
->leftJoin("{$komoditasTable} as k", 'k.id', '=', 'p.komoditas_id')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as nama")
|
||||
->selectRaw('COUNT(p.id) as petani_count')
|
||||
->where('k.is_utama', true)
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->orderByDesc('petani_count')
|
||||
->get();
|
||||
|
||||
$totalPetani = (int) $komoditasCollection->sum('petani_count');
|
||||
// $totalPetani = (int) $komoditasCollection->sum('petani_count');
|
||||
$totalPetani = Petani::count();
|
||||
$circumference = 2 * pi() * 46;
|
||||
|
||||
$komoditas = $komoditasCollection
|
||||
@@ -89,36 +96,34 @@ class SurveyPetaniController extends Controller
|
||||
// Progres pengisian data per petani
|
||||
|
||||
$progressPetani = Petani::query()
|
||||
->select([
|
||||
'id',
|
||||
'nama',
|
||||
])
|
||||
->withExists([
|
||||
'lahans as has_lahan',
|
||||
'praktikBudidaya as has_praktik_budidaya',
|
||||
'sosialEkonomi as has_sosial_ekonomi',
|
||||
'aksesPasar as has_akses_pasar',
|
||||
->select(['id', 'nama', 'komoditas_id'])
|
||||
->with([
|
||||
'komoditas:id,nama',
|
||||
'lahans',
|
||||
'praktikBudidaya',
|
||||
'sosialEkonomi',
|
||||
'aksesPasar',
|
||||
])
|
||||
->orderBy('nama')
|
||||
->get()
|
||||
->map(function ($petani) {
|
||||
$status = [
|
||||
'lahan' => [
|
||||
'label' => 'Lahan',
|
||||
'done' => (bool) $petani->has_lahan,
|
||||
],
|
||||
'praktik_budidaya' => [
|
||||
'label' => 'Praktik Budidaya',
|
||||
'done' => (bool) $petani->has_praktik_budidaya,
|
||||
],
|
||||
'sosial_ekonomi' => [
|
||||
'label' => 'Sosial Ekonomi',
|
||||
'done' => (bool) $petani->has_sosial_ekonomi,
|
||||
],
|
||||
'akses_pasar' => [
|
||||
'label' => 'Akses Pasar',
|
||||
'done' => (bool) $petani->has_akses_pasar,
|
||||
],
|
||||
'lahan' => $this->moduleStatus(
|
||||
'Lahan',
|
||||
$this->hasCompleteLahan($petani)
|
||||
),
|
||||
'praktik_budidaya' => $this->moduleStatus(
|
||||
'Praktik Budidaya',
|
||||
$this->hasCompletePraktikBudidaya($petani->praktikBudidaya)
|
||||
),
|
||||
'sosial_ekonomi' => $this->moduleStatus(
|
||||
'Sosial Ekonomi',
|
||||
$this->hasCompleteSosialEkonomi($petani->sosialEkonomi)
|
||||
),
|
||||
'akses_pasar' => $this->moduleStatus(
|
||||
'Akses Pasar',
|
||||
$this->hasCompleteAksesPasar($petani->aksesPasar)
|
||||
),
|
||||
];
|
||||
|
||||
$totalModul = count($status);
|
||||
@@ -128,12 +133,13 @@ class SurveyPetaniController extends Controller
|
||||
->count();
|
||||
|
||||
return [
|
||||
'id' => $petani->id,
|
||||
'name' => $petani->nama,
|
||||
'status' => $status,
|
||||
'done' => $modulSelesai,
|
||||
'total' => $totalModul,
|
||||
'percent' => $totalModul > 0
|
||||
'id' => $petani->id,
|
||||
'name' => $petani->nama,
|
||||
'komoditas' => $petani->komoditas?->nama ?? 'Belum ditentukan',
|
||||
'status' => $status,
|
||||
'done' => $modulSelesai,
|
||||
'total' => $totalModul,
|
||||
'percent' => $totalModul > 0
|
||||
? (int) round(($modulSelesai / $totalModul) * 100)
|
||||
: 0,
|
||||
];
|
||||
@@ -142,23 +148,30 @@ class SurveyPetaniController extends Controller
|
||||
|
||||
// Ringkasan lahan
|
||||
|
||||
$totalLuasLahan = (float) Lahan::query()
|
||||
->sum('luas_lahan');
|
||||
|
||||
$totalJumlahTanaman = (int) Lahan::query()
|
||||
->sum('jumlah_tanaman');
|
||||
// $totalLuasLahan = ((float) Lahan::query()
|
||||
// ->sum('luas_lahan')) / 10000;
|
||||
$totalLuasLahan = Lahan::has('petani')->sum('luas_lahan');
|
||||
|
||||
// Jumlah tanaman berdasarkan komoditas petani
|
||||
|
||||
$tanamanPerKomoditasCollection = DB::table("{$lahanTable} as l")
|
||||
$tanamanTable = (new Tanaman())->getTable();
|
||||
|
||||
$tanamanPerKomoditasCollection = DB::table("{$tanamanTable} as t")
|
||||
->join("{$lahanTable} as l", 'l.id', '=', 't.lahan_id')
|
||||
->join("{$petaniTable} as p", 'p.id', '=', 'l.petani_id')
|
||||
->leftJoin("{$komoditasTable} as k", 'k.id', '=', 'p.komoditas_id')
|
||||
->leftJoin("{$komoditasTable} as k", 'k.id', '=', 't.komoditas_id')
|
||||
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||
->selectRaw('COALESCE(SUM(l.jumlah_tanaman), 0) as total')
|
||||
->selectRaw('COUNT(t.id) as total')
|
||||
->where('k.is_utama', true)
|
||||
->whereNull('t.deleted_at')
|
||||
->whereNull('l.deleted_at')
|
||||
->whereNull('p.deleted_at')
|
||||
->groupBy('k.id', 'k.nama')
|
||||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
$totalJumlahTanaman = (int) $tanamanPerKomoditasCollection->sum('total');
|
||||
|
||||
$tanamanPerKomoditas = $tanamanPerKomoditasCollection
|
||||
->map(function ($item, $index) use ($colors, $totalJumlahTanaman) {
|
||||
$color = $colors[$index % count($colors)];
|
||||
@@ -268,6 +281,147 @@ class SurveyPetaniController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function moduleStatus(string $label, bool $done): array
|
||||
{
|
||||
return [
|
||||
'label' => $label,
|
||||
'done' => $done,
|
||||
];
|
||||
}
|
||||
|
||||
private function hasCompleteLahan(Petani $petani): bool
|
||||
{
|
||||
return $petani->lahans->contains(function ($lahan) {
|
||||
return $this->allFieldsFilled($lahan, [
|
||||
'luas_lahan',
|
||||
'komoditas_id',
|
||||
'jenis_komoditas_id',
|
||||
'umur_tanaman',
|
||||
'kondisi_tanaman',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function hasCompletePraktikBudidaya(?Model $praktikBudidaya): bool
|
||||
{
|
||||
return $this->allFieldsFilled($praktikBudidaya, [
|
||||
'frekuensi_pemangkasan_utama',
|
||||
'pemberian_pupuk',
|
||||
'metode_penanganan_opt',
|
||||
'frekuensi_sanitasi_lahan',
|
||||
'penanganan_gulma',
|
||||
'kondisi_pengelolaan_air',
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasCompleteSosialEkonomi(?Model $sosialEkonomi): bool
|
||||
{
|
||||
return $this->allFieldsFilled($sosialEkonomi, [
|
||||
'skor_periode_paceklik',
|
||||
'skor_kemampuan_menabung',
|
||||
'skor_kecukupan_penghasilan',
|
||||
'skor_kejelasan_status_lahan',
|
||||
'kemauan_mencoba_praktik_baru',
|
||||
'pemahaman_mutu_harga',
|
||||
'panutan_petani_sukses',
|
||||
'kemauan_ikut_program',
|
||||
'orientasi_rencana_jangka_panjang',
|
||||
'keaktifan_pertemuan_kelompok',
|
||||
'peran_dalam_kelompok',
|
||||
'manfaat_kelompok_tani',
|
||||
'status_rekening_tabungan',
|
||||
'pengalaman_kredit_kebun',
|
||||
'kondisi_aset_produktif',
|
||||
'keterlibatan_program_keuangan_formal',
|
||||
'keterikatan_hutang_panen',
|
||||
'praktik_ijon',
|
||||
'kebebasan_memilih_pembeli',
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasCompleteAksesPasar(?Model $aksesPasar): bool
|
||||
{
|
||||
if (! $this->allFieldsFilled($aksesPasar, [
|
||||
'skor_pengalaman',
|
||||
'praktik_fermentasi',
|
||||
'skor_kesadaran_premium',
|
||||
'tingkat_kepercayaan_pembeli_baru',
|
||||
])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$skorPengalaman = (int) $aksesPasar->getAttribute('skor_pengalaman');
|
||||
|
||||
if (
|
||||
$skorPengalaman !== SkorPengalaman::BELUM_PERNAH->value
|
||||
&& ! $this->valueIsFilled($aksesPasar->getAttribute('saluran_terdahulu'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array($skorPengalaman, [
|
||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value,
|
||||
], true)
|
||||
&& ! $this->valueIsFilled($aksesPasar->getAttribute('alasan_berhenti'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array(
|
||||
SaluranTerdahulu::LAINNYA->value,
|
||||
$aksesPasar->getAttribute('saluran_terdahulu') ?? [],
|
||||
true
|
||||
)
|
||||
&& ! $this->valueIsFilled($aksesPasar->getAttribute('saluran_terdahulu_lainnya'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$aksesPasar->getAttribute('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value
|
||||
&& ! $this->allFieldsFilled($aksesPasar, [
|
||||
'proses_fermentasi',
|
||||
'model_fermentasi_memungkinkan',
|
||||
])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function allFieldsFilled(?Model $model, array $fields): bool
|
||||
{
|
||||
if (! $model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! $this->valueIsFilled($model->getAttribute($field))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function valueIsFilled(mixed $value): bool
|
||||
{
|
||||
if ($value instanceof BackedEnum) {
|
||||
$value = $value->value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return collect($value)
|
||||
->contains(fn ($item) => $this->valueIsFilled($item));
|
||||
}
|
||||
|
||||
return ! is_null($value) && $value !== '';
|
||||
}
|
||||
|
||||
private function pelatihanGapOptions(): array
|
||||
{
|
||||
$enumClass = self::GAP_ENUM_CLASS;
|
||||
@@ -335,34 +489,34 @@ class SurveyPetaniController extends Controller
|
||||
{
|
||||
return [
|
||||
[
|
||||
'color' => '#28a745',
|
||||
'bg' => 'rgba(40,167,69,.15)',
|
||||
'tx' => '#28a745',
|
||||
'color' => '#6FD68F',
|
||||
'bg' => 'rgba(111,214,143,.24)',
|
||||
'tx' => '#6FD68F',
|
||||
],
|
||||
[
|
||||
'color' => '#007bff',
|
||||
'bg' => 'rgba(0,123,255,.15)',
|
||||
'tx' => '#007bff',
|
||||
'color' => '#5FC7D6',
|
||||
'bg' => 'rgba(95,199,214,.24)',
|
||||
'tx' => '#5FC7D6',
|
||||
],
|
||||
[
|
||||
'color' => '#fd7e14',
|
||||
'bg' => 'rgba(253,126,20,.15)',
|
||||
'tx' => '#fd7e14',
|
||||
'color' => '#FFB46F',
|
||||
'bg' => 'rgba(255,180,111,.26)',
|
||||
'tx' => '#FFB46F',
|
||||
],
|
||||
[
|
||||
'color' => '#dc3545',
|
||||
'bg' => 'rgba(220,53,69,.15)',
|
||||
'tx' => '#dc3545',
|
||||
'color' => '#F47C86',
|
||||
'bg' => 'rgba(244,124,134,.26)',
|
||||
'tx' => '#F47C86',
|
||||
],
|
||||
[
|
||||
'color' => '#6f42c1',
|
||||
'bg' => 'rgba(111,66,193,.15)',
|
||||
'tx' => '#6f42c1',
|
||||
'color' => '#9D8EF0',
|
||||
'bg' => 'rgba(157,142,240,.26)',
|
||||
'tx' => '#9D8EF0',
|
||||
],
|
||||
[
|
||||
'color' => '#20c997',
|
||||
'bg' => 'rgba(32,201,151,.15)',
|
||||
'tx' => '#20c997',
|
||||
'color' => '#65D8BD',
|
||||
'bg' => 'rgba(101,216,189,.26)',
|
||||
'tx' => '#65D8BD',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -55,19 +55,7 @@ class FormBasisDataController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'parent_id' => ['nullable', 'exists:ekosistem_basis_data_form,id'],
|
||||
'tipe' => ['required', Rule::enum(LayoutForm::class)],
|
||||
'column_width' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::COLUMN->value),
|
||||
'nullable', 'integer', 'min:1', 'max:12'],
|
||||
'kolom_id' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::VALUE->value),
|
||||
'nullable', 'exists:ekosistem_basis_data_kolom,id'],
|
||||
'section_label' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::SECTION->value),
|
||||
'nullable', 'string'],
|
||||
]);
|
||||
$validated = $this->validateFormPayload($request, $tabelId);
|
||||
|
||||
FormBasisData::create([...$validated,
|
||||
'tabel_id' => $tabelId,
|
||||
@@ -115,19 +103,7 @@ class FormBasisDataController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'parent_id' => ['nullable', 'exists:ekosistem_basis_data_form,id'],
|
||||
'tipe' => ['required', Rule::enum(LayoutForm::class)],
|
||||
'column_width' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::COLUMN->value),
|
||||
'nullable', 'integer', 'min:1', 'max:12'],
|
||||
'kolom_id' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::VALUE->value),
|
||||
'nullable', 'exists:ekosistem_basis_data_kolom,id'],
|
||||
'section_label' => [
|
||||
Rule::requiredIf($request->tipe === LayoutForm::SECTION->value),
|
||||
'nullable', 'string'],
|
||||
]);
|
||||
$validated = $this->validateFormPayload($request, $tabelId, $form);
|
||||
|
||||
$form->update([...$validated,
|
||||
'updated_by' => auth()->user()->id
|
||||
@@ -293,4 +269,88 @@ class FormBasisDataController extends Controller
|
||||
'message' => 'Formulir basis data berhasil diperbarui.'
|
||||
], 200);
|
||||
}
|
||||
|
||||
private function validateFormPayload(Request $request, string $tabelId, ?FormBasisData $form = null): array
|
||||
{
|
||||
$tipe = $request->input('tipe');
|
||||
$needsParent = in_array($tipe, [LayoutForm::COLUMN->value, LayoutForm::VALUE->value], true);
|
||||
$mustBeRoot = in_array($tipe, [LayoutForm::ROW->value, LayoutForm::SECTION->value, LayoutForm::HR_LINE->value], true);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'parent_id' => [
|
||||
Rule::requiredIf($needsParent),
|
||||
Rule::prohibitedIf($mustBeRoot),
|
||||
'nullable',
|
||||
Rule::exists('ekosistem_basis_data_form', 'id')
|
||||
->where(fn ($query) => $query
|
||||
->where('tabel_id', $tabelId)
|
||||
->whereNull('deleted_at')),
|
||||
],
|
||||
'tipe' => ['required', Rule::enum(LayoutForm::class)],
|
||||
'column_width' => [
|
||||
Rule::requiredIf($tipe === LayoutForm::COLUMN->value),
|
||||
'nullable', 'integer', 'min:1', 'max:12'
|
||||
],
|
||||
'kolom_id' => [
|
||||
Rule::requiredIf($tipe === LayoutForm::VALUE->value),
|
||||
'nullable',
|
||||
Rule::exists('ekosistem_basis_data_kolom', 'id')
|
||||
->where(fn ($query) => $query
|
||||
->where('tabel_id', $tabelId)
|
||||
->whereNull('deleted_at')),
|
||||
],
|
||||
'section_label' => [
|
||||
Rule::requiredIf($tipe === LayoutForm::SECTION->value),
|
||||
'nullable', 'string'
|
||||
],
|
||||
]);
|
||||
|
||||
$validator->after(function ($validator) use ($request, $tabelId, $form) {
|
||||
if ($validator->errors()->isNotEmpty() || !$request->filled('parent_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($form && (int) $request->input('parent_id') === (int) $form->id) {
|
||||
$validator->errors()->add('parent_id', 'Induk formulir tidak boleh menunjuk ke dirinya sendiri.');
|
||||
return;
|
||||
}
|
||||
|
||||
$layout = LayoutForm::from($request->input('tipe'));
|
||||
$expectedParent = match ($layout) {
|
||||
LayoutForm::COLUMN => LayoutForm::ROW,
|
||||
LayoutForm::VALUE => LayoutForm::COLUMN,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (!$expectedParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent = FormBasisData::where('tabel_id', $tabelId)->find($request->input('parent_id'));
|
||||
if (!$parent || $parent->tipe !== $expectedParent) {
|
||||
$validator->errors()->add('parent_id', 'Induk formulir tidak sesuai dengan tipe formulir.');
|
||||
}
|
||||
});
|
||||
|
||||
$validated = $validator->validate();
|
||||
$layout = LayoutForm::from($validated['tipe']);
|
||||
|
||||
if (in_array($layout, [LayoutForm::ROW, LayoutForm::SECTION, LayoutForm::HR_LINE], true)) {
|
||||
$validated['parent_id'] = null;
|
||||
}
|
||||
|
||||
if ($layout !== LayoutForm::COLUMN) {
|
||||
$validated['column_width'] = null;
|
||||
}
|
||||
|
||||
if ($layout !== LayoutForm::VALUE) {
|
||||
$validated['kolom_id'] = null;
|
||||
}
|
||||
|
||||
if ($layout !== LayoutForm::SECTION) {
|
||||
$validated['section_label'] = null;
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,15 @@ class AuthController extends Controller
|
||||
public function createAccount(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'username' => 'required|string|max:255|unique:users,username',
|
||||
'username' => 'required|string|min:6|max:255|unique:users,username',
|
||||
'email' => 'required|string|email|max:255|unique:users,email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
], [
|
||||
'username.unique' => 'Nama pengguna sudah digunakan.',
|
||||
'username.min' => 'Nama pengguna minimal 6 karakter.',
|
||||
'email.unique' => 'Email sudah digunakan.',
|
||||
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
||||
'password.min' => 'Password minimal 8 karakter.',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
@@ -89,6 +95,9 @@ class AuthController extends Controller
|
||||
$request->validate([
|
||||
'email_username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
], [
|
||||
'email_username.required' => 'Email atau username harus diisi.',
|
||||
'password.required' => 'Password harus diisi.',
|
||||
]);
|
||||
|
||||
$login = $request->input('email_username');
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,28 +112,34 @@ 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)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'username' => ['required', 'string', 'max:255', 'unique:users,username'],
|
||||
'username' => ['required', 'string', 'max:255', 'min:6', 'unique:users,username'],
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'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' => ['required'],
|
||||
'desa_kelurahan_id' => ['nullable', 'exists:desa_kelurahan,id'],
|
||||
'ktp' => ['nullable', 'min:16', 'max:16'],
|
||||
'kk' => ['nullable', 'min:16', 'max:16'],
|
||||
'file_ktp' => ['nullable', 'file', 'mimes:jpeg,jpg,png,pdf', 'max:2048'],
|
||||
'file_kk' => ['nullable', 'file', 'mimes:jpeg,jpg,pdf,png', 'max:2048'],
|
||||
], [
|
||||
'username.unique' => 'Nama pengguna sudah digunakan.',
|
||||
'username.min' => 'Nama pengguna minimal 6 karakter.',
|
||||
'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.'
|
||||
]);
|
||||
|
||||
@@ -161,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,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Koperasi;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Koperasi\PengurusKoperasi;
|
||||
|
||||
class PengurusKoperasiController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = PengurusKoperasi::query();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where('nama_lengkap', 'like', "%{$search}%")
|
||||
->orWhere('nama_koperasi', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$perPage = $request->get('per_page', 10);
|
||||
$pengurus = $query->latest()->paginate($perPage);
|
||||
|
||||
$totalPengurus = PengurusKoperasi::count();
|
||||
$totalKoperasi = PengurusKoperasi::distinct('nama_koperasi')->count('nama_koperasi');
|
||||
$totalJabatan = PengurusKoperasi::distinct('posisi_dalam_koperasi')->count('posisi_dalam_koperasi');
|
||||
|
||||
return view('koperasi.profil.index', compact(
|
||||
'pengurus',
|
||||
'totalPengurus',
|
||||
'totalKoperasi',
|
||||
'totalJabatan',
|
||||
));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('koperasi.profil.add');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$pengurus = PengurusKoperasi::findOrFail($id);
|
||||
return view('koperasi.profil.edit', compact('pengurus'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'nama_lengkap' => 'required|string|max:255',
|
||||
'posisi_dalam_koperasi' => 'required|string|max:255',
|
||||
'tahun_bergabung' => 'nullable|integer',
|
||||
'no_hp' => 'nullable|string|max:20',
|
||||
'alamat_tinggal' => 'nullable|string',
|
||||
'nama_koperasi' => 'required|string|max:255',
|
||||
'jenis_koperasi' => 'required|string|max:255',
|
||||
'jenis_koperasi_lainnya'=> 'nullable|string|max:255',
|
||||
'alamat_koperasi' => 'nullable|string',
|
||||
'skala_operasi' => 'required|string|max:255',
|
||||
'tahun_berdiri' => 'nullable|integer',
|
||||
'jumlah_anggota_awal' => 'nullable|integer',
|
||||
'jumlah_anggota_dewasa' => 'nullable|integer',
|
||||
'jumlah_anggota_anak' => 'nullable|integer',
|
||||
'jumlah_pengurus' => 'nullable|integer',
|
||||
'shu_2025' => 'nullable|numeric',
|
||||
'shu_2024' => 'nullable|numeric',
|
||||
'shu_2023' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
if ($validatedData['jenis_koperasi'] === 'Lainnya' && !empty($validatedData['jenis_koperasi_lainnya'])) {
|
||||
$validatedData['jenis_koperasi'] = $validatedData['jenis_koperasi_lainnya'];
|
||||
}
|
||||
unset($validatedData['jenis_koperasi_lainnya']);
|
||||
|
||||
$pengurus = PengurusKoperasi::findOrFail($id);
|
||||
$pengurus->update($validatedData);
|
||||
|
||||
return redirect()->route('koperasi.pengurus.index')
|
||||
->with('success', 'Profil Koperasi berhasil diperbarui!');
|
||||
}
|
||||
|
||||
public function survey(Request $request, $id)
|
||||
{
|
||||
$pengurus = PengurusKoperasi::findOrFail($id);
|
||||
$step = $request->query('step', 1);
|
||||
|
||||
if ($step == 1) {
|
||||
return view('koperasi.kelembagaan-aktor.index', compact('pengurus'));
|
||||
} elseif ($step == 2) {
|
||||
return view('koperasi.rantai-nilai-akses-pasar.index', compact('pengurus'));
|
||||
} elseif ($step == 3) {
|
||||
return view('koperasi.sosial-ekonomi.index', compact('pengurus'));
|
||||
}
|
||||
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function surveyStore(Request $request, $id)
|
||||
{
|
||||
$pengurus = PengurusKoperasi::findOrFail($id);
|
||||
$step = $request->query('step', 1);
|
||||
$data = $request->except(['_token', 'pengurus_koperasi_id']);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$data[$key] = json_encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
$pengurus->update($data);
|
||||
|
||||
if ($step == 1) {
|
||||
if ($request->has('agregasi_komoditas') && str_contains($data['agregasi_komoditas'], 'Tidak terlibat')) {
|
||||
return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 3])
|
||||
->with('success', 'Data Kelembagaan tersimpan! (Melewati Rantai Nilai)');
|
||||
}
|
||||
|
||||
return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 2])
|
||||
->with('success', 'Data Kelembagaan berhasil disimpan!');
|
||||
}
|
||||
|
||||
if ($step == 2) {
|
||||
return redirect()->route('koperasi.pengurus.survey', ['id' => $id, 'step' => 3])
|
||||
->with('success', 'Data Rantai Nilai berhasil disimpan!');
|
||||
}
|
||||
|
||||
if ($step == 3) {
|
||||
return redirect()->route('koperasi.pengurus.index')
|
||||
->with('success', 'Seluruh data survei berhasil diselesaikan dan disimpan!');
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'nama_lengkap' => 'required|string|max:255',
|
||||
'posisi_dalam_koperasi' => 'required|string|max:255',
|
||||
'tahun_bergabung' => 'nullable|integer',
|
||||
'no_hp' => 'nullable|string|max:20',
|
||||
'alamat_tinggal' => 'nullable|string',
|
||||
'nama_koperasi' => 'required|string|max:255',
|
||||
'jenis_koperasi' => 'required|string|max:255',
|
||||
'jenis_koperasi_lainnya'=> 'nullable|string|max:255',
|
||||
'alamat_koperasi' => 'nullable|string',
|
||||
'skala_operasi' => 'required|string|max:255',
|
||||
'tahun_berdiri' => 'nullable|integer',
|
||||
'jumlah_anggota_awal' => 'nullable|integer',
|
||||
'jumlah_anggota_dewasa' => 'nullable|integer',
|
||||
'jumlah_anggota_anak' => 'nullable|integer',
|
||||
'jumlah_pengurus' => 'nullable|integer',
|
||||
'shu_2025' => 'nullable|numeric',
|
||||
'shu_2024' => 'nullable|numeric',
|
||||
'shu_2023' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
if ($validatedData['jenis_koperasi'] === 'Lainnya' && !empty($validatedData['jenis_koperasi_lainnya'])) {
|
||||
$validatedData['jenis_koperasi'] = $validatedData['jenis_koperasi_lainnya'];
|
||||
}
|
||||
unset($validatedData['jenis_koperasi_lainnya']);
|
||||
|
||||
$pengurus = PengurusKoperasi::create($validatedData);
|
||||
|
||||
return redirect()->route('koperasi.pengurus.index')
|
||||
->with('success', 'Profil Koperasi berhasil ditambahkan! Silakan klik tombol hijau untuk mulai survei.');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$pengurus = PengurusKoperasi::findOrFail($id);
|
||||
$pengurus->delete();
|
||||
|
||||
return redirect()->route('koperasi.pengurus.index')
|
||||
->with('success', 'Data pengurus koperasi berhasil dihapus!');
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,13 @@ class DesaKelurahanController extends Controller
|
||||
return response()->json($desa);
|
||||
}
|
||||
|
||||
public function getByKecamatan($kecamatan_id)
|
||||
public function getByKecamatan(Request $request, $kecamatan_id)
|
||||
{
|
||||
$desa = DesaKelurahan::where('kecamatan_id', $kecamatan_id)
|
||||
$query = $request->boolean('all_wilayah')
|
||||
? DesaKelurahan::withoutGlobalScope('program')
|
||||
: DesaKelurahan::query();
|
||||
|
||||
$desa = $query->where('kecamatan_id', $kecamatan_id)
|
||||
->orderBy('nama', 'asc')
|
||||
->get();
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ class KabupatenKotaController extends Controller
|
||||
return response()->json($kabupaten);
|
||||
}
|
||||
|
||||
public function getByProvinsi($provinsi_id)
|
||||
public function getByProvinsi(Request $request, $provinsi_id)
|
||||
{
|
||||
$kabupaten = KabupatenKota::where('provinsi_id', $provinsi_id)
|
||||
$query = $request->boolean('all_wilayah')
|
||||
? KabupatenKota::withoutGlobalScope('program')
|
||||
: KabupatenKota::query();
|
||||
|
||||
$kabupaten = $query->where('provinsi_id', $provinsi_id)
|
||||
->orderBy('nama', 'asc')
|
||||
->get();
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ class KecamatanController extends Controller
|
||||
return response()->json($kecamatan);
|
||||
}
|
||||
|
||||
public function getByKabupaten($kabupaten_id)
|
||||
public function getByKabupaten(Request $request, $kabupaten_id)
|
||||
{
|
||||
$kecamatan = Kecamatan::where('kabupaten_kota_id', $kabupaten_id)
|
||||
$query = $request->boolean('all_wilayah')
|
||||
? Kecamatan::withoutGlobalScope('program')
|
||||
: Kecamatan::query();
|
||||
|
||||
$kecamatan = $query->where('kabupaten_kota_id', $kabupaten_id)
|
||||
->orderBy('nama', 'asc')
|
||||
->get();
|
||||
|
||||
|
||||
@@ -28,9 +28,13 @@ class KomoditasController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama' => ['required', 'string', 'max:255', 'unique:master_komoditas,nama'], // Tambah unique biar nggak ada data kembar
|
||||
'nama' => ['required', 'string', 'max:255', 'unique:master_komoditas,nama'],
|
||||
'kode' => ['required', 'string', 'max:2', 'unique:master_komoditas,kode'],
|
||||
'is_utama' => ['nullable', 'boolean'],
|
||||
], [
|
||||
'nama.unique' => 'Nama komoditas ini sudah ada di database.'
|
||||
'nama.unique' => 'Nama komoditas ini sudah ada di database.',
|
||||
'kode.unique' => 'Kode komoditas ini sudah ada di database.',
|
||||
'kode.max' => 'Kode komoditas maksimal 2 Digit.'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -39,6 +43,8 @@ class KomoditasController extends Controller
|
||||
|
||||
Komoditas::create([
|
||||
'nama' => $request->nama,
|
||||
'kode' => $request->kode,
|
||||
'is_utama' => $request->is_utama,
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
@@ -55,9 +61,13 @@ class KomoditasController extends Controller
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama' => ['required', 'string', 'max:255', 'unique:master_komoditas,nama,' . $id],
|
||||
'nama' => ['required', 'string', 'max:255', 'unique:master_komoditas,nama,' . $id],
|
||||
'kode' => ['required', 'string', 'max:2', 'unique:master_komoditas,kode,' . $id],
|
||||
'is_utama' => ['nullable', 'boolean'],
|
||||
], [
|
||||
'nama.unique' => 'Nama komoditas ini sudah digunakan.'
|
||||
'nama.unique' => 'Nama komoditas ini sudah ada di database.',
|
||||
'kode.unique' => 'Kode komoditas ini sudah ada di database.',
|
||||
'kode.max' => 'Kode komoditas maksimal 2 Digit.'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -66,8 +76,10 @@ class KomoditasController extends Controller
|
||||
|
||||
$komoditas = Komoditas::findOrFail($id);
|
||||
$komoditas->update([
|
||||
'nama' => $request->nama,
|
||||
'updated_by' => auth()->id(),
|
||||
'nama' => $request->nama,
|
||||
'kode' => $request->kode,
|
||||
'is_utama' => $request->is_utama,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Komoditas berhasil diupdate.']);
|
||||
|
||||
@@ -16,49 +16,74 @@ use App\Enums\AksesPasar\SkorPengalaman;
|
||||
use App\Enums\AksesPasar\SaluranTerdahulu;
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Enums\AksesPasar\SkorKesadaranPremium;
|
||||
use App\Enums\AksesPasar\TingkatKepercayaanPembeli;
|
||||
|
||||
class AksesPasarController extends Controller
|
||||
{
|
||||
public function index($petaniId)
|
||||
{
|
||||
$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',
|
||||
'Biji basah',
|
||||
'Biji kering/Biji Kupas',
|
||||
'Terfermentasi (Rp/kg)',
|
||||
])->map(function (string $bentuk, int $index) use ($hargaJualPanen) {
|
||||
$existingData = $hargaJualPanen->firstWhere('bentuk_produk', $bentuk);
|
||||
|
||||
return [
|
||||
'index' => $index,
|
||||
'bentuk' => $bentuk,
|
||||
'harga_panen_terakhir' => $this->currencyForDisplay($existingData?->harga_panen_terakhir),
|
||||
'harga_tahun_lalu' => $this->currencyForDisplay($existingData?->harga_tahun_lalu),
|
||||
'harga_dua_tahun_lalu' => $this->currencyForDisplay($existingData?->harga_dua_tahun_lalu),
|
||||
];
|
||||
});
|
||||
|
||||
return view('produktivitas.petani.akses-pasar.index', array_merge(
|
||||
compact('petani', 'aksesPasar', 'hargaJualPanen'),
|
||||
compact('petani', 'aksesPasar', 'hargaJualPanen', 'hargaJualRows'),
|
||||
$this->aksesPasarOptions()
|
||||
));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -97,10 +122,12 @@ class AksesPasarController extends Controller
|
||||
'praktikFermentasiOptions' => PraktikFermentasi::options(),
|
||||
'praktikBelumPernahValue' => PraktikFermentasi::BELUM_PERNAH->value,
|
||||
'skorKesadaranPremiumOptions' => SkorKesadaranPremium::options(),
|
||||
'tingkatKepercayaanOptions' => TingkatKepercayaanPembeli::options(),
|
||||
'skorPengalamanBerhentiValues' => [
|
||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
||||
]
|
||||
],
|
||||
'sumberInfoLainnyaValue' => 'Lainnya',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -110,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'), [
|
||||
@@ -125,6 +163,14 @@ class AksesPasarController extends Controller
|
||||
Rule::requiredIf(fn () => in_array(SaluranTerdahulu::LAINNYA->value, $request->input('saluran_terdahulu', []), true)),
|
||||
'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' => [
|
||||
@@ -138,6 +184,9 @@ class AksesPasarController extends Controller
|
||||
|
||||
'skor_kesadaran_premium' => ['nullable', 'integer', Rule::in(SkorKesadaranPremium::values())],
|
||||
'sumber_info_premium' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
'tingkat_kepercayaan_pembeli_baru' => ['nullable', 'integer', Rule::in(TingkatKepercayaanPembeli::values())],
|
||||
'alasan_kepercayaan_pembeli_baru' => ['nullable', 'string'],
|
||||
|
||||
'harga_jual' => ['nullable', 'array'],
|
||||
'harga_jual.*.bentuk_produk' => ['nullable', 'string'],
|
||||
@@ -147,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.',
|
||||
@@ -183,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
|
||||
@@ -194,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;
|
||||
@@ -202,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();
|
||||
@@ -251,4 +308,11 @@ class AksesPasarController extends Controller
|
||||
$digits = preg_replace('/\D/', '', (string) $value);
|
||||
return $digits === '' ? null : $digits;
|
||||
}
|
||||
|
||||
private function currencyForDisplay(mixed $value): string
|
||||
{
|
||||
$digits = $this->currencyDigits($value);
|
||||
|
||||
return is_null($digits) ? '' : number_format((int) $digits, 0, ',', '.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
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\AsetKelompokTani;
|
||||
|
||||
class AsetKelompokTaniController extends Controller
|
||||
{
|
||||
public const JENIS_ASET_BAKU = [
|
||||
'Bibit/alat produksi',
|
||||
'Pupuk (stok kelompok)',
|
||||
'Pulper/alat kupas',
|
||||
'Lantai jemur',
|
||||
'Kotak/wadah fermentasi',
|
||||
'Kas/simpanan kelompok',
|
||||
'Gudang/tempat simpan',
|
||||
'Timbangan',
|
||||
];
|
||||
|
||||
private function authorizeAccess(): void
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||
{
|
||||
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();
|
||||
|
||||
return redirect($this->kegiatanUrl($kelompokTani) . '#section-aset-baku');
|
||||
}
|
||||
|
||||
public function update(Request $request, KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$validated = $request->validate([
|
||||
'aset' => ['required', 'array'],
|
||||
'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)],
|
||||
'aset.*.status_aset' => ['nullable', 'boolean'],
|
||||
'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.',
|
||||
]);
|
||||
|
||||
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);
|
||||
|
||||
$existing = $kelompokTani->aset()->where('jenis_aset', $jenisAset)->first()
|
||||
?? $kelompokTani->aset()->onlyTrashed()->where('jenis_aset', $jenisAset)->latest('id')->first();
|
||||
|
||||
if (!$ada) {
|
||||
if ($existing && !$existing->trashed()) {
|
||||
$existing->update(['deleted_by' => auth()->id()]);
|
||||
$existing->delete();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->trashed()) {
|
||||
$existing->restore();
|
||||
}
|
||||
|
||||
$existing->update([
|
||||
'status_aset' => true,
|
||||
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
|
||||
'kondisi_aset' => $item['kondisi_aset'],
|
||||
'deleted_by' => null,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$kelompokTani->aset()->create([
|
||||
'uid' => 'AKT-' . strtoupper(Str::random(10)),
|
||||
'jenis_aset' => $jenisAset,
|
||||
'status_aset' => true,
|
||||
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
|
||||
'kondisi_aset' => $item['kondisi_aset'],
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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($this->asetLainnyaRules(), [
|
||||
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
|
||||
]);
|
||||
|
||||
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 === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kelompokTani->aset()->create([
|
||||
'uid' => 'AKT-' . strtoupper(Str::random(10)),
|
||||
'jenis_aset' => $jenisAset,
|
||||
'status_aset' => true,
|
||||
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? '-',
|
||||
'kondisi_aset' => $item['kondisi_aset'] ?? '-',
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404);
|
||||
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($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class BantuanKelompokTaniController extends Controller
|
||||
{
|
||||
private function authorizeAccess(): void
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||
{
|
||||
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
|
||||
'success' => $message,
|
||||
'success_section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
private function redirectToSectionWithError(string $url, string $section, string $message)
|
||||
{
|
||||
return redirect($url . '#kelompok-tani-notifications')->with([
|
||||
'error' => $message,
|
||||
'error_section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
return redirect($this->operasionalUrl($kelompokTani) . '#section-status-bantuan');
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
if (!$kelompokTani->pernah_mendapatkan_bantuan) {
|
||||
return $this->redirectToSectionWithError(
|
||||
$this->operasionalUrl($kelompokTani),
|
||||
'status-bantuan',
|
||||
'Operasional: pilih jawaban "Pernah" terlebih dahulu sebelum menambahkan data bantuan/program.'
|
||||
);
|
||||
}
|
||||
|
||||
$validated = $request->validate($this->bantuanRules(true, !$this->hasRecordedAid($kelompokTani)), $this->bantuanMessages());
|
||||
|
||||
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' => $namaProgram,
|
||||
'penyelenggara' => $item['penyelenggara'] ?? '',
|
||||
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? '',
|
||||
'tahun' => $item['tahun'] ?? null,
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
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($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(),
|
||||
@@ -77,10 +91,11 @@ class PendidikanPelatihanController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Riwayat pelatihan berhasil disimpan.');
|
||||
return $this->redirectBackToFragment($request, 'section-sertifikasi')
|
||||
->with('success', 'Riwayat pelatihan berhasil disimpan.');
|
||||
}
|
||||
|
||||
public function destroyPelatihan(Petani $petani, SurveyPelatihan $surveyPelatihan)
|
||||
public function destroyPelatihan(Request $request, Petani $petani, SurveyPelatihan $surveyPelatihan)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
@@ -92,7 +107,8 @@ class PendidikanPelatihanController extends Controller
|
||||
|
||||
$surveyPelatihan->delete();
|
||||
|
||||
return back()->with('success', 'Riwayat pelatihan berhasil dihapus.');
|
||||
return $this->redirectBackToFragment($request, 'section-pelatihan')
|
||||
->with('success', 'Riwayat pelatihan berhasil dihapus.');
|
||||
}
|
||||
|
||||
public function storeSertifikasi(Request $request, Petani $petani)
|
||||
@@ -155,10 +171,11 @@ class PendidikanPelatihanController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Sertifikasi berhasil disimpan.');
|
||||
return $this->redirectBackToFragment($request, 'section-sertifikasi')
|
||||
->with('success', 'Sertifikasi berhasil disimpan.');
|
||||
}
|
||||
|
||||
public function destroySertifikasi(Petani $petani, SurveySertifikasi $surveySertifikasi)
|
||||
public function destroySertifikasi(Request $request, Petani $petani, SurveySertifikasi $surveySertifikasi)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
@@ -170,6 +187,18 @@ class PendidikanPelatihanController extends Controller
|
||||
|
||||
$surveySertifikasi->delete();
|
||||
|
||||
return back()->with('success', 'Sertifikasi berhasil dihapus.');
|
||||
return $this->redirectBackToFragment($request, 'section-sertifikasi')
|
||||
->with('success', 'Sertifikasi berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectBackToFragment(Request $request, string $defaultFragment)
|
||||
{
|
||||
$fragment = $request->input('redirect_fragment', $defaultFragment);
|
||||
|
||||
if (!is_string($fragment) || !preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
|
||||
$fragment = $defaultFragment;
|
||||
}
|
||||
|
||||
return redirect()->to(Str::before(url()->previous(), '#') . '#' . $fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Produktivitas;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Models\Pertanian\KelompokTani;
|
||||
use App\Models\Pertanian\PengurusKelompokTani;
|
||||
|
||||
class PengurusKelompokTaniController extends Controller
|
||||
{
|
||||
private function authorizeAccess(): void
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToSection(string $url, string $section, string $message)
|
||||
{
|
||||
return redirect($url . '#section-' . $section)->with([
|
||||
'success' => $message,
|
||||
'success_section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
return redirect(route('kelompok-tani.edit', $kelompokTani->id) . '#section-ringkasan');
|
||||
}
|
||||
|
||||
public function store(Request $request, KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$validated = $request->validate([
|
||||
'pengurus' => ['nullable', 'array'],
|
||||
'pengurus.*.nama_pengurus' => ['nullable', 'string', 'max:255'],
|
||||
'pengurus.*.jabatan' => ['nullable', 'string', 'max:255'],
|
||||
'pengurus.*.kontak' => ['nullable', 'string', 'max:50'],
|
||||
'pengurus.*.status' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
foreach ($validated['pengurus'] ?? [] as $item) {
|
||||
if (empty($item['nama_pengurus']) && empty($item['jabatan']) && empty($item['kontak'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kelompokTani->pengurus()->create([
|
||||
'uid' => 'PKT-' . strtoupper(Str::random(10)),
|
||||
'nama_pengurus' => $item['nama_pengurus'] ?? null,
|
||||
'jabatan' => $item['jabatan'] ?? null,
|
||||
'kontak' => $item['kontak'] ?? null,
|
||||
'status' => $item['status'] ?? false,
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
abort_unless($pengurus->survey_kelompok_tani_id === $kelompokTani->id, 404);
|
||||
|
||||
$pengurus->update(['deleted_by' => auth()->id()]);
|
||||
$pengurus->delete();
|
||||
|
||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,14 @@ use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use App\Models\Wilayah\Provinsi;
|
||||
use App\Models\Wilayah\KabupatenKota;
|
||||
use App\Models\Wilayah\Kecamatan;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Tanaman;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
|
||||
use App\Enums\Pendidikan;
|
||||
use App\Enums\Pekerjaan;
|
||||
@@ -27,26 +32,205 @@ class PetaniController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$query = Petani::with('desakelurahan')
|
||||
->orderBy('created_at', 'desc');
|
||||
$provinsi = Provinsi::select('id', 'kode', 'nama')->orderBy('kode', 'asc')->get();
|
||||
$userRole = auth()->user()->role->value;
|
||||
$showFasilitatorColumn = in_array($userRole, ['super_admin', 'admin', 'koordinator'], true);
|
||||
$defaultKabupaten = KabupatenKota::select('id', 'nama')
|
||||
->whereRaw('lower(nama) like ?', ['%ngada%'])
|
||||
->orderBy('nama')
|
||||
->first();
|
||||
$selectedKabupatenId = $request->query('kabupaten_kota_id', $defaultKabupaten?->id);
|
||||
$allowedSorts = ['nama_asc', 'nama_desc', 'luas_lahan_desc', 'luas_lahan_asc'];
|
||||
$requestedSort = $request->query('sort_by')
|
||||
?? match ($request->query('sort_nama')) {
|
||||
'asc' => 'nama_asc',
|
||||
'desc' => 'nama_desc',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($request->search) {
|
||||
$query->where(function ($query) use ($request) {
|
||||
$query->where('nama', 'like', '%' . $request->search . '%')
|
||||
->orWhere('ktp', 'like', '%' . $request->search . '%');
|
||||
$filters = [
|
||||
'search' => $request->query('search'),
|
||||
'komoditas_id' => $request->query('komoditas_id'),
|
||||
'kabupaten_kota_id' => $selectedKabupatenId,
|
||||
'kecamatan_id' => $request->query('kecamatan_id'),
|
||||
'desa_kelurahan_id' => $request->query('desa_kelurahan_id'),
|
||||
'sort_by' => in_array($requestedSort, $allowedSorts, true)
|
||||
? $requestedSort
|
||||
: null,
|
||||
];
|
||||
|
||||
$query = Petani::with([
|
||||
'desaKelurahan:id,nama,kecamatan_id',
|
||||
'desaKelurahan.kecamatan:id,nama,kabupaten_kota_id',
|
||||
'desaKelurahan.kecamatan.kabupatenKota:id,nama,provinsi_id',
|
||||
'desaKelurahan.kecamatan.kabupatenKota.provinsi:id,nama',
|
||||
'komoditas:id,nama',
|
||||
'skor:id,petani_id,skor_kesiapan_adopsi',
|
||||
'creator:id,name,role',
|
||||
])
|
||||
->withSum('lahans as total_luas_lahan', 'luas_lahan');
|
||||
|
||||
$search = $request->filled('search') ? Str::lower($request->search) : null;
|
||||
if ($search) {
|
||||
$query->where(function ($query) use ($search) {
|
||||
$query->whereRaw('lower(nama) like ?', ["%{$search}%"])
|
||||
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('komoditas_id')) {
|
||||
$query->where('komoditas_id', $request->komoditas_id);
|
||||
}
|
||||
|
||||
if ($filters['kabupaten_kota_id']) {
|
||||
$query->whereHas('desaKelurahan.kecamatan', function ($query) use ($filters) {
|
||||
$query->where('kabupaten_kota_id', $filters['kabupaten_kota_id']);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('kecamatan_id')) {
|
||||
$query->whereHas('desaKelurahan', function ($query) use ($request) {
|
||||
$query->where('kecamatan_id', $request->kecamatan_id);
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('desa_kelurahan_id')) {
|
||||
$query->where('desa_kelurahan_id', $request->desa_kelurahan_id);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
match ($filters['sort_by']) {
|
||||
'nama_asc' => $query->orderBy('nama', 'asc')->orderBy('created_at', 'desc'),
|
||||
'nama_desc' => $query->orderBy('nama', 'desc')->orderBy('created_at', 'desc'),
|
||||
'luas_lahan_desc' => $query->orderByDesc(
|
||||
Lahan::selectRaw('COALESCE(SUM(luas_lahan), 0)')
|
||||
->whereColumn('map_lahan.petani_id', 'petani.id')
|
||||
->whereNull('map_lahan.deleted_at')
|
||||
)->orderBy('created_at', 'desc'),
|
||||
'luas_lahan_asc' => $query->orderBy(
|
||||
Lahan::selectRaw('COALESCE(SUM(luas_lahan), 0)')
|
||||
->whereColumn('map_lahan.petani_id', 'petani.id')
|
||||
->whereNull('map_lahan.deleted_at')
|
||||
)->orderBy('created_at', 'desc'),
|
||||
default => $query->orderBy('created_at', 'desc'),
|
||||
};
|
||||
|
||||
$summaryPetaniIds = (clone $query)->pluck('id');
|
||||
$petani = $query->paginate($request->per_page ?? 10);
|
||||
|
||||
return view('produktivitas.petani.index', compact('petani', 'provinsi'));
|
||||
$komoditas = Komoditas::select('id', 'nama')->where('is_utama', true)->get();
|
||||
$komoditasOptions = Komoditas::select('id', 'nama')->orderBy('nama')->get();
|
||||
$kabupatenOptions = KabupatenKota::select('id', 'nama')->orderBy('nama')->get();
|
||||
$selectedKabupaten = $filters['kabupaten_kota_id']
|
||||
? KabupatenKota::select('id', 'nama')->find($filters['kabupaten_kota_id'])
|
||||
: null;
|
||||
$kecamatanOptions = $filters['kabupaten_kota_id']
|
||||
? Kecamatan::select('id', 'nama')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get()
|
||||
: collect();
|
||||
$desaOptions = $request->filled('kecamatan_id')
|
||||
? DesaKelurahan::select('id', 'nama')->where('kecamatan_id', $request->kecamatan_id)->orderBy('nama')->get()
|
||||
: collect();
|
||||
$lahanSummaryQuery = Lahan::query()
|
||||
->whereHas('petani')
|
||||
->when($request->filled('komoditas_id'), function ($query) use ($request) {
|
||||
$query->where('komoditas_id', $request->komoditas_id);
|
||||
})
|
||||
->when($filters['kabupaten_kota_id'], function ($query) use ($filters) {
|
||||
$query->whereHas('desaKelurahan.kecamatan', function ($kecamatan) use ($filters) {
|
||||
$kecamatan->where('kabupaten_kota_id', $filters['kabupaten_kota_id']);
|
||||
});
|
||||
})
|
||||
->when($request->filled('kecamatan_id'), function ($query) use ($request) {
|
||||
$query->whereHas('desaKelurahan', function ($desa) use ($request) {
|
||||
$desa->where('kecamatan_id', $request->kecamatan_id);
|
||||
});
|
||||
})
|
||||
->when($request->filled('desa_kelurahan_id'), function ($query) use ($request) {
|
||||
$query->where('desa_kelurahan_id', $request->desa_kelurahan_id);
|
||||
})
|
||||
->when($request->filled('search'), function ($query) use ($search) {
|
||||
$query->whereHas('petani', function ($petani) use ($search) {
|
||||
$petani->whereRaw('lower(nama) like ?', ["%{$search}%"])
|
||||
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
|
||||
});
|
||||
})
|
||||
->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();
|
||||
$totalLuasLahan = (clone $lahanSummaryQuery)->sum('luas_lahan');
|
||||
$totalTanaman = Tanaman::whereIn('lahan_id', (clone $lahanSummaryQuery)->select('id'))
|
||||
->when($request->filled('komoditas_id'), function ($query) use ($request) {
|
||||
$query->where('komoditas_id', $request->komoditas_id);
|
||||
})
|
||||
->count();
|
||||
|
||||
$case = "
|
||||
CASE
|
||||
WHEN skor_kesiapan_adopsi >= 4.0 THEN 'Penggerak'
|
||||
WHEN skor_kesiapan_adopsi >= 3.0 THEN 'Perintis'
|
||||
WHEN skor_kesiapan_adopsi >= 2.0 THEN 'Pemerhati'
|
||||
WHEN skor_kesiapan_adopsi >= 1.0 THEN 'Pemula'
|
||||
ELSE 'Belum Lengkap'
|
||||
END
|
||||
";
|
||||
$skor = SkorPetani::selectRaw("$case AS segmen, COUNT(*) AS total")
|
||||
->whereIn('petani_id', $summaryPetaniIds)
|
||||
->groupByRaw($case)
|
||||
->get();
|
||||
|
||||
return view('produktivitas.petani.index', compact(
|
||||
'petani',
|
||||
'provinsi',
|
||||
'komoditas',
|
||||
'komoditasOptions',
|
||||
'kabupatenOptions',
|
||||
'selectedKabupaten',
|
||||
'kecamatanOptions',
|
||||
'desaOptions',
|
||||
'filters',
|
||||
'skor',
|
||||
'totalLahan',
|
||||
'totalLuasLahan',
|
||||
'totalTanaman',
|
||||
'totalPetani',
|
||||
'totalPetaniLakiLaki',
|
||||
'totalPetaniPerempuan',
|
||||
'showFasilitatorColumn'
|
||||
));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -54,7 +238,7 @@ class PetaniController extends Controller
|
||||
$pekerjaan = Pekerjaan::options();
|
||||
$pernikahan = Pernikahan::options();
|
||||
|
||||
$komoditas = Komoditas::all();
|
||||
$komoditas = Komoditas::where('is_utama', true)->get();
|
||||
|
||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||
|
||||
@@ -118,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);
|
||||
}
|
||||
|
||||
@@ -145,12 +329,12 @@ class PetaniController extends Controller
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id()]);
|
||||
|
||||
return redirect()->route('petani.index')->with('success', 'Data petani berhasil ditambahkan.');
|
||||
return redirect()->route('petani.edit', $petani->id)->with('success', 'Data petani berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function show(string $id)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -162,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);
|
||||
}
|
||||
|
||||
@@ -174,7 +358,7 @@ class PetaniController extends Controller
|
||||
|
||||
$provinsi = Provinsi::all();
|
||||
|
||||
$komoditas = Komoditas::all();
|
||||
$komoditas = Komoditas::where('is_utama', true)->get();
|
||||
|
||||
return view('produktivitas.petani.edit', compact('petani', 'pendidikan', 'pekerjaan',
|
||||
'pernikahan', 'provinsi', 'komoditas'));
|
||||
@@ -183,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);
|
||||
}
|
||||
|
||||
@@ -191,6 +375,13 @@ class PetaniController extends Controller
|
||||
|
||||
$validatedData = $this->validatePetani($request);
|
||||
|
||||
if ($request->input('hapus_foto') == '1') {
|
||||
if ($petani->foto && Storage::exists($petani->foto)) {
|
||||
Storage::delete($petani->foto);
|
||||
}
|
||||
$validatedData['foto'] = null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('file_ktp')) {
|
||||
$file_ktp = $request->file('file_ktp');
|
||||
$validatedData['path_ktp'] = $file_ktp->store('petani/ktp');
|
||||
@@ -202,6 +393,10 @@ class PetaniController extends Controller
|
||||
$validatedData['file_kk'] = $file_kk->getClientOriginalName();
|
||||
}
|
||||
if ($request->hasFile('foto')) {
|
||||
|
||||
if ($petani->foto && Storage::exists($petani->foto)) {
|
||||
Storage::delete($petani->foto);
|
||||
}
|
||||
$file_foto = $request->file('foto');
|
||||
$validatedData['foto'] = $file_foto->store('petani/foto');
|
||||
}
|
||||
@@ -209,20 +404,46 @@ class PetaniController extends Controller
|
||||
$petani->update([...$validatedData,
|
||||
'updated_by' => auth()->id()]);
|
||||
|
||||
return redirect()->route('petani.index')->with('success', 'Data petani berhasil diupdate.');
|
||||
return redirect()->route('petani.edit', $petani->id)->with('success', 'Data petani berhasil diupdate.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
//
|
||||
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 === 'enumerator' && $petani->created_by !== auth()->id()) {
|
||||
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($petani) {
|
||||
$petani->update([
|
||||
'deleted_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
$petani->delete();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Data petani berhasil dihapus.'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal menghapus data: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function showImage(string $id, string $field)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
@@ -17,7 +18,9 @@ use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use App\Models\Wilayah\Provinsi;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\PraktikBudidaya;
|
||||
use App\Models\Pertanian\Tanaman;
|
||||
use App\Models\Master\JenisTanaman;
|
||||
|
||||
use App\Enums\StatusLahan;
|
||||
use App\Enums\KondisiTanaman;
|
||||
@@ -29,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);
|
||||
}
|
||||
|
||||
@@ -49,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);
|
||||
}
|
||||
|
||||
@@ -62,8 +65,47 @@ class PetaniLahanController extends Controller
|
||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||
|
||||
$komoditas = Komoditas::all();
|
||||
$tanamanPenaung = JenisTanaman::all();
|
||||
$autoPestDiseaseData = $this->autoPestDiseaseData($petaniId);
|
||||
$jenisHamaOtomatis = $autoPestDiseaseData['jenis_hama'];
|
||||
$jenisPenyakitOtomatis = $autoPestDiseaseData['jenis_penyakit'];
|
||||
|
||||
return view('produktivitas.petani.lahan.add', compact('petani', 'lahan', 'provinsi', 'komoditas'));
|
||||
return view('produktivitas.petani.lahan.add', compact('petani', 'lahan', 'provinsi', 'komoditas', 'tanamanPenaung', 'jenisHamaOtomatis', 'jenisPenyakitOtomatis'));
|
||||
}
|
||||
|
||||
private function autoPestDiseaseData(string|int $petaniId): array
|
||||
{
|
||||
$praktikBudidaya = PraktikBudidaya::where('petani_id', $petaniId)->latest()->first();
|
||||
|
||||
return [
|
||||
'jenis_hama' => $this->normalizeArrayValue($praktikBudidaya?->jenis_hama),
|
||||
'jenis_penyakit' => $this->normalizeArrayValue($praktikBudidaya?->jenis_penyakit),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeArrayValue(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_values(array_filter($value, fn ($item) => filled($item)));
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
return array_values(array_filter($decoded, fn ($item) => filled($item)));
|
||||
}
|
||||
|
||||
return [$value];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function hasPestDiseaseColumns(string $table): bool
|
||||
{
|
||||
return Schema::hasColumn($table, 'jenis_hama')
|
||||
&& Schema::hasColumn($table, 'jenis_penyakit');
|
||||
}
|
||||
|
||||
private function validateLahan(Request $request)
|
||||
@@ -81,8 +123,8 @@ class PetaniLahanController extends Controller
|
||||
'komoditas_id' => ['required', 'exists:master_komoditas,id'],
|
||||
'jenis_komoditas_id' => ['required', 'exists:jenis_komoditas,id'],
|
||||
'varietas_id' => ['nullable', 'exists:master_varietas,id'],
|
||||
'jumlah_tanaman' => ['required', 'numeric', 'min:1'],
|
||||
'umur_lahan' => ['required', 'numeric', 'min:1'],
|
||||
'jumlah_tanaman' => ['nullable', 'numeric', 'min:0'],
|
||||
'umur_lahan' => ['nullable', 'numeric', 'min:1'],
|
||||
'umur_tanaman' => ['required', 'numeric', 'min:1'],
|
||||
'kondisi_tanaman' => ['required', 'string', new Enum(KondisiTanaman::class)],
|
||||
'serangan_hama' => ['nullable', 'string', new Enum(SeranganHama::class)],
|
||||
@@ -91,7 +133,7 @@ class PetaniLahanController extends Controller
|
||||
'komoditas_lain' => ['nullable', 'array'],
|
||||
'komoditas_lain.*' => ['integer', 'exists:master_komoditas,id'],
|
||||
'tanaman_penaung' => ['nullable', 'array'],
|
||||
'tanaman_penaung.*' => ['integer', 'exists:master_komoditas,id'],
|
||||
'tanaman_penaung.*' => ['string'],
|
||||
'foto' => ['nullable', 'array'],
|
||||
'foto.*' => ['image', 'mimes:jpeg,jpg,png', 'max:2048'],
|
||||
], [
|
||||
@@ -102,10 +144,10 @@ class PetaniLahanController extends Controller
|
||||
'luas_lahan.required' => 'Luas lahan wajib diisi.',
|
||||
'luas_lahan.numeric' => 'Luas lahan diisi dengan angka.',
|
||||
'luas_lahan.min' => 'Luas lahan minimal 0.01.',
|
||||
'jumlah_tanaman.required' => 'Jumlah tanaman wajib diisi.',
|
||||
//'jumlah_tanaman.required' => 'Jumlah tanaman wajib diisi.',
|
||||
'jumlah_tanaman.numeric' => 'Jumlah tanaman diisi dengan angka.',
|
||||
'jumlah_tanaman.min' => 'Jumlah tanaman minimal 1.',
|
||||
'umur_lahan.required' => 'Umur lahan wajib diisi.',
|
||||
'jumlah_tanaman.min' => 'Jumlah tanaman minimal 0.',
|
||||
//'umur_lahan.required' => 'Umur lahan wajib diisi.',
|
||||
'umur_lahan.numeric' => 'Umur lahan diisi dengan angka.',
|
||||
'umur_lahan.min' => 'Umur lahan minimal 1.',
|
||||
'umur_tanaman.required' => 'Umur tanaman wajib diisi.',
|
||||
@@ -119,7 +161,7 @@ class PetaniLahanController extends Controller
|
||||
'komoditas_lain.array' => 'Komoditas lain diisi dengan array.',
|
||||
'komoditas_lain.*.exists' => 'Komoditas lain yang dipilih tidak valid.',
|
||||
'tanaman_penaung.array' => 'Tanaman penaung diisi dengan array.',
|
||||
'tanaman_penaung.*.exists' => 'Tanaman penaung yang dipilih tidak valid.',
|
||||
//'tanaman_penaung.*.exists' => 'Tanaman penaung yang dipilih tidak valid.',
|
||||
'foto.array' => 'Foto diisi dengan array.',
|
||||
'foto.*.image' => 'Foto harus berupa gambar.',
|
||||
'foto.*.mimes' => 'Foto harus berupa gambar dengan ekstensi jpeg, jpg, atau png.',
|
||||
@@ -128,20 +170,57 @@ class PetaniLahanController extends Controller
|
||||
|
||||
}
|
||||
|
||||
private function syncTanamanPenaung(Request $request)
|
||||
{
|
||||
$existing = JenisTanaman::pluck('nama')->toArray();
|
||||
|
||||
$tanamanPenaung = collect($request->tanaman_penaung)
|
||||
->unique()
|
||||
->reject(fn ($nama) => in_array($nama, $existing))
|
||||
->map(fn ($nama) => [
|
||||
'nama' => Str::title($nama),
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
if (!empty($tanamanPenaung)) {
|
||||
JenisTanaman::insert($tanamanPenaung);
|
||||
}
|
||||
}
|
||||
|
||||
public function store($petaniId, Request $request)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$petani = Petani::findOrFail($petaniId);
|
||||
|
||||
$request->merge([
|
||||
'tanaman_penaung' => collect($request->tanaman_penaung)
|
||||
->map(fn ($nama) => Str::title($nama))
|
||||
->toArray()
|
||||
]);
|
||||
|
||||
$validated = $this->validateLahan($request);
|
||||
$autoPestDiseaseData = $this->autoPestDiseaseData($petaniId);
|
||||
$lahanHasPestDiseaseColumns = $this->hasPestDiseaseColumns((new Lahan)->getTable());
|
||||
$tanamanHasPestDiseaseColumns = $this->hasPestDiseaseColumns((new Tanaman)->getTable());
|
||||
|
||||
if ($lahanHasPestDiseaseColumns) {
|
||||
$validated = array_merge($validated, $autoPestDiseaseData);
|
||||
}
|
||||
|
||||
$komoditas = Komoditas::where('id', $request->komoditas_id)->first();
|
||||
$lahanCount = Lahan::where('petani_id', $petaniId)
|
||||
->count();
|
||||
$lahanUid = strtoupper(Str::random(10));
|
||||
|
||||
|
||||
$foto = null;
|
||||
if ($request->hasFile('foto')) {
|
||||
$foto = [];
|
||||
foreach ($request->file('foto') as $file) {
|
||||
@@ -153,6 +232,8 @@ class PetaniLahanController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$this->syncTanamanPenaung($request);
|
||||
|
||||
$lahan = Lahan::create([...$validated,
|
||||
'foto' => $foto && count($foto) > 0 ? json_encode($foto) : null,
|
||||
'nama' => 'Lahan ' . $komoditas->nama . ' #' . ($lahanCount + 1),
|
||||
@@ -162,9 +243,9 @@ class PetaniLahanController extends Controller
|
||||
'updated_by' => auth()->id()]
|
||||
);
|
||||
|
||||
if ($lahan) {
|
||||
$data = collect(range(1, $lahan->jumlah_tanaman))->map(function ($i) use ($lahan, $komoditas, $lahanUid) {
|
||||
return [
|
||||
if ($lahan && $lahan->jumlah_tanaman > 0) {
|
||||
$data = collect(range(1, $lahan->jumlah_tanaman))->map(function ($i) use ($lahan, $komoditas, $lahanUid, $autoPestDiseaseData, $tanamanHasPestDiseaseColumns) {
|
||||
$row = [
|
||||
'lahan_id' => $lahan->id,
|
||||
'uid' => 'TNM-' . $lahanUid . '-' . strtoupper(Str::random(10)),
|
||||
'komoditas_id' => $lahan->komoditas_id,
|
||||
@@ -182,19 +263,36 @@ class PetaniLahanController extends Controller
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
if ($tanamanHasPestDiseaseColumns) {
|
||||
$row['jenis_hama'] = json_encode($autoPestDiseaseData['jenis_hama'] ?? []);
|
||||
$row['jenis_penyakit'] = json_encode($autoPestDiseaseData['jenis_penyakit'] ?? []);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
$data->chunk(500)->each(function ($chunk) {
|
||||
Tanaman::insert($chunk->toArray());
|
||||
});
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.index', $petaniId)->with('success', 'Lahan berhasil ditambahkan.');
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'redirect' => route('petani.lahan-tanaman.edit', [$petaniId, $lahan->id]),
|
||||
'message' => 'Lahan berhasil ditambahkan.'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.edit', [$petaniId, $lahan->id])->with('success', 'Lahan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.edit', [$petaniId, $lahan->id])->with('success', 'Lahan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function show(string $petaniId, string $id)
|
||||
{
|
||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -212,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);
|
||||
}
|
||||
|
||||
@@ -225,29 +323,81 @@ class PetaniLahanController extends Controller
|
||||
$lahanSelected = Lahan::where('petani_id', $petaniId)->findOrFail($id);
|
||||
|
||||
$komoditas = Komoditas::all();
|
||||
$tanamanPenaung = JenisTanaman::all();
|
||||
$autoPestDiseaseData = $this->autoPestDiseaseData($petaniId);
|
||||
$jenisHamaOtomatis = $autoPestDiseaseData['jenis_hama'];
|
||||
$jenisPenyakitOtomatis = $autoPestDiseaseData['jenis_penyakit'];
|
||||
|
||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||
|
||||
return view('produktivitas.petani.lahan.edit', compact('lahan', 'lahanSelected', 'petani', 'komoditas', 'provinsi'));
|
||||
return view('produktivitas.petani.lahan.edit', compact('lahan', 'lahanSelected', 'petani', 'komoditas', 'provinsi', 'tanamanPenaung', 'jenisHamaOtomatis', 'jenisPenyakitOtomatis'));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$petani = Petani::findOrFail($petaniId);
|
||||
$lahan = Lahan::where('petani_id', $petaniId)->findOrFail($id);
|
||||
|
||||
$request->merge([
|
||||
'tanaman_penaung' => collect($request->tanaman_penaung)
|
||||
->map(fn ($nama) => Str::title($nama))
|
||||
->toArray()
|
||||
]);
|
||||
$validated = $this->validateLahan($request);
|
||||
$autoPestDiseaseData = $this->autoPestDiseaseData($petaniId);
|
||||
$lahanHasPestDiseaseColumns = $this->hasPestDiseaseColumns((new Lahan)->getTable());
|
||||
$tanamanHasPestDiseaseColumns = $this->hasPestDiseaseColumns((new Tanaman)->getTable());
|
||||
|
||||
if ($lahanHasPestDiseaseColumns) {
|
||||
$validated = array_merge($validated, $autoPestDiseaseData);
|
||||
}
|
||||
|
||||
$komoditas = Komoditas::where('id', $request->komoditas_id)->first();
|
||||
$tanamanCount = Tanaman::where('lahan_id', $lahan->id)->count();
|
||||
$lahanCount = Lahan::where('petani_id', $petaniId)->count();
|
||||
|
||||
$fotoLama = $lahan->foto ?? [];
|
||||
if (is_string($fotoLama)) {
|
||||
$fotoLama = json_decode($fotoLama, true);
|
||||
}
|
||||
if (!is_array($fotoLama)) {
|
||||
$fotoLama = [];
|
||||
}
|
||||
|
||||
if ($request->has('deleted_photos') && is_array($request->deleted_photos)) {
|
||||
foreach ($request->deleted_photos as $deletedPath) {
|
||||
|
||||
if (Storage::disk('public')->exists($deletedPath)) {
|
||||
Storage::disk('public')->delete($deletedPath);
|
||||
}
|
||||
|
||||
$fotoLama = array_filter($fotoLama, function($item) use ($deletedPath) {
|
||||
$currentPath = is_array($item) ? ($item['path'] ?? '') : $item;
|
||||
return $currentPath !== $deletedPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$fotoLama = array_values($fotoLama);
|
||||
|
||||
if ($request->hasFile('foto')) {
|
||||
foreach ($request->file('foto') as $file) {
|
||||
$path = $file->store('lahan', 'public');
|
||||
$fotoLama[] = [
|
||||
'path' => $path,
|
||||
'file' => $file->getClientOriginalName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
unset($validated['foto']);
|
||||
|
||||
$lahan->update([...$validated,
|
||||
//'nama' => 'Lahan ' . $komoditas->nama . ' #' . ($lahanCount + 1),
|
||||
//'petani_id' => $petaniId,
|
||||
'foto' => count($fotoLama) > 0 ? json_encode($fotoLama) : null,
|
||||
'updated_by' => auth()->id()]
|
||||
);
|
||||
|
||||
@@ -255,9 +405,11 @@ class PetaniLahanController extends Controller
|
||||
$lahanUidParts = explode('LHN-', $lahanUid);
|
||||
$lahanUid = end($lahanUidParts);
|
||||
|
||||
$this->syncTanamanPenaung($request);
|
||||
|
||||
if ($lahan && $lahan->jumlah_tanaman > $tanamanCount) {
|
||||
$data = collect(range($tanamanCount + 1, $lahan->jumlah_tanaman))->map(function ($i) use ($lahan, $komoditas, $lahanUid) {
|
||||
return [
|
||||
$data = collect(range($tanamanCount + 1, $lahan->jumlah_tanaman))->map(function ($i) use ($lahan, $komoditas, $lahanUid, $autoPestDiseaseData, $tanamanHasPestDiseaseColumns) {
|
||||
$row = [
|
||||
'lahan_id' => $lahan->id,
|
||||
'komoditas_id' => $lahan->komoditas_id,
|
||||
'jenis_komoditas_id' => $lahan->jenis_komoditas_id,
|
||||
@@ -275,6 +427,13 @@ class PetaniLahanController extends Controller
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
if ($tanamanHasPestDiseaseColumns) {
|
||||
$row['jenis_hama'] = json_encode($autoPestDiseaseData['jenis_hama'] ?? []);
|
||||
$row['jenis_penyakit'] = json_encode($autoPestDiseaseData['jenis_penyakit'] ?? []);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
$data->chunk(500)->each(function ($chunk) {
|
||||
@@ -282,17 +441,47 @@ class PetaniLahanController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.index', $petaniId)->with('success', 'Lahan berhasil ditambahkan.');
|
||||
if ($tanamanHasPestDiseaseColumns) {
|
||||
$lahan->tanaman()->update([
|
||||
'jenis_hama' => json_encode($autoPestDiseaseData['jenis_hama'] ?? []),
|
||||
'jenis_penyakit' => json_encode($autoPestDiseaseData['jenis_penyakit'] ?? []),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'redirect' => route('petani.lahan-tanaman.edit', [$petaniId, $lahan->id]),
|
||||
'message' => 'Lahan berhasil diupdate.'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.edit', [$petaniId, $lahan->id])->with('success', 'Lahan berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(string $id)
|
||||
public function destroy(string $petaniId, string $id)
|
||||
{
|
||||
//
|
||||
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$lahan = Lahan::findOrFail($id);
|
||||
$lahan->tanaman()->update([
|
||||
'deleted_by' => auth()->id()
|
||||
]);
|
||||
$lahan->tanaman()->delete();
|
||||
$lahan->update([
|
||||
'deleted_by' => auth()->id()
|
||||
]);
|
||||
$lahan->delete();
|
||||
|
||||
return redirect()->route('petani.lahan-tanaman.index', $petaniId)->with('success', 'Lahan berhasil dihapus.');
|
||||
}
|
||||
|
||||
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,58 @@
|
||||
<?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 wajib menyetujui pernyataan kelengkapan saat menandai lengkap.
|
||||
'pernyataan' => ['exclude_unless:status,' . StatusDataPetani::LENGKAP->value, 'accepted'],
|
||||
], [
|
||||
'pernyataan.accepted' => 'Centang 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.');
|
||||
}
|
||||
|
||||
$petani->status = $tujuan;
|
||||
$petani->updated_by = $request->user()->id;
|
||||
$petani->save();
|
||||
|
||||
return back()->with('status_petani_success', $tujuan === StatusDataPetani::LENGKAP
|
||||
? 'Data petani berhasil ditandai lengkap.'
|
||||
: 'Tanda lengkap berhasil dibatalkan. Status kembali ke draf.');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -178,8 +178,20 @@ class PraktikBudidayaController extends Controller
|
||||
|
||||
$teknik_pemangkasan = array_values($teknik_pemangkasan);
|
||||
|
||||
if ((int) ($validatedData['frekuensi_pemangkasan_komoditas_utama'] ?? 0) === FrekuensiPemangkasanUtama::TIDAK_PERNAH->value) {
|
||||
$teknik_pemangkasan = [];
|
||||
}
|
||||
|
||||
$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([
|
||||
'petani_id' => $petani->id,
|
||||
'uid' => 'PB-' . strtoupper(Str::random(10)),
|
||||
@@ -193,12 +205,12 @@ class PraktikBudidayaController extends Controller
|
||||
'deskripsi_penyerbukan_manual' => $validatedData['alasan_tidak_penyerbukan'] ?? null,
|
||||
'tingkat_keberhasilan_penyerbukan' => $validatedData['tingkat_keberhasilan_penyerbukan'] ?? null,
|
||||
'pemberian_pupuk' => $validatedData['pemberian_pupuk'] ?? null,
|
||||
'jenis_pupuk' => $validatedData['jenis_pupuk'] ?? null,
|
||||
'frekuensi_pemberian_pupuk' => ($validatedData['frekuensi_pemupukan'] ?? null) === 'Lainnya' ? ($validatedData['frekuensi_pemupukan_lainnya'] ?? null) : ($validatedData['frekuensi_pemupukan'] ?? null),
|
||||
'estimasi_dosis_pupuk_organik' => $validatedData['estimasi_dosis_pupuk_organik'] ?? null,
|
||||
'estimasi_dosis_pupuk_anorganik' => $validatedData['estimasi_dosis_pupuk_anorganik'] ?? null,
|
||||
'waktu_pemberian_pupuk' => ($validatedData['waktu_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['waktu_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['waktu_pemberian_pupuk'] ?? null),
|
||||
'cara_pemberian_pupuk' => ($validatedData['cara_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['cara_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['cara_pemberian_pupuk'] ?? null),
|
||||
'jenis_pupuk' => $pemupukanTidakPernah ? null : ($validatedData['jenis_pupuk'] ?? null),
|
||||
'frekuensi_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['frekuensi_pemupukan'] ?? null) === 'Lainnya' ? ($validatedData['frekuensi_pemupukan_lainnya'] ?? null) : ($validatedData['frekuensi_pemupukan'] ?? null)),
|
||||
'estimasi_dosis_pupuk_organik' => $pemupukanTidakPernah ? null : ($validatedData['estimasi_dosis_pupuk_organik'] ?? null),
|
||||
'estimasi_dosis_pupuk_anorganik' => $pemupukanTidakPernah ? null : ($validatedData['estimasi_dosis_pupuk_anorganik'] ?? null),
|
||||
'waktu_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['waktu_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['waktu_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['waktu_pemberian_pupuk'] ?? null)),
|
||||
'cara_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['cara_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['cara_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['cara_pemberian_pupuk'] ?? null)),
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
], [
|
||||
@@ -235,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);
|
||||
}
|
||||
|
||||
@@ -328,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);
|
||||
}
|
||||
|
||||
@@ -554,7 +566,19 @@ class PraktikBudidayaController extends Controller
|
||||
}
|
||||
}
|
||||
$teknikPemangkasan = array_values($teknik_pemangkasan);
|
||||
|
||||
|
||||
if ((int) ($validatedData['frekuensi_pemangkasan_komoditas_utama'] ?? 0) === FrekuensiPemangkasanUtama::TIDAK_PERNAH->value) {
|
||||
$teknikPemangkasan = [];
|
||||
}
|
||||
|
||||
$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'],
|
||||
@@ -566,12 +590,12 @@ class PraktikBudidayaController extends Controller
|
||||
'deskripsi_penyerbukan_manual' => $validatedData['alasan_tidak_penyerbukan'] ?? null,
|
||||
'tingkat_keberhasilan_penyerbukan' => $validatedData['tingkat_keberhasilan_penyerbukan'] ?? null,
|
||||
'pemberian_pupuk' => $validatedData['pemberian_pupuk'] ?? null,
|
||||
'jenis_pupuk' => $validatedData['jenis_pupuk'] ?? null,
|
||||
'frekuensi_pemberian_pupuk' => ($validatedData['frekuensi_pemupukan'] ?? null) === 'Lainnya' ? ($validatedData['frekuensi_pemupukan_lainnya'] ?? null) : ($validatedData['frekuensi_pemupukan'] ?? null),
|
||||
'estimasi_dosis_pupuk_organik' => $validatedData['estimasi_dosis_pupuk_organik'] ?? null,
|
||||
'estimasi_dosis_pupuk_anorganik' => $validatedData['estimasi_dosis_pupuk_anorganik'] ?? null,
|
||||
'waktu_pemberian_pupuk' => ($validatedData['waktu_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['waktu_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['waktu_pemberian_pupuk'] ?? null),
|
||||
'cara_pemberian_pupuk' => ($validatedData['cara_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['cara_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['cara_pemberian_pupuk'] ?? null),
|
||||
'jenis_pupuk' => $pemupukanTidakPernah ? null : ($validatedData['jenis_pupuk'] ?? null),
|
||||
'frekuensi_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['frekuensi_pemupukan'] ?? null) === 'Lainnya' ? ($validatedData['frekuensi_pemupukan_lainnya'] ?? null) : ($validatedData['frekuensi_pemupukan'] ?? null)),
|
||||
'estimasi_dosis_pupuk_organik' => $pemupukanTidakPernah ? null : ($validatedData['estimasi_dosis_pupuk_organik'] ?? null),
|
||||
'estimasi_dosis_pupuk_anorganik' => $pemupukanTidakPernah ? null : ($validatedData['estimasi_dosis_pupuk_anorganik'] ?? null),
|
||||
'waktu_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['waktu_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['waktu_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['waktu_pemberian_pupuk'] ?? null)),
|
||||
'cara_pemberian_pupuk' => $pemupukanTidakPernah ? null : (($validatedData['cara_pemberian_pupuk'] ?? null) === 'Lainnya' ? ($validatedData['cara_pemberian_pupuk_lainnya'] ?? null) : ($validatedData['cara_pemberian_pupuk'] ?? null)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -670,11 +694,22 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Data berhasil diperbarui.');
|
||||
return $this->redirectBackToFragment($request)->with('success', 'Data berhasil diperbarui.');
|
||||
}
|
||||
|
||||
private function redirectBackToFragment(Request $request)
|
||||
{
|
||||
$fragment = $request->input('redirect_fragment');
|
||||
|
||||
if (!is_string($fragment) || !preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
|
||||
return back();
|
||||
}
|
||||
|
||||
return redirect()->to(Str::before(url()->previous(), '#') . '#' . $fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Produktivitas;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\Tanaman;
|
||||
use App\Models\Pertanian\PraktikBudidaya;
|
||||
use App\Models\Pertanian\SurveyPanen;
|
||||
use App\Models\Pertanian\SosialEkonomi;
|
||||
use App\Models\Pertanian\SurveyPelatihan;
|
||||
use App\Models\Pertanian\SurveySertifikasi;
|
||||
use App\Models\Pertanian\AksesPasar;
|
||||
use App\Models\Pertanian\HargaJualPanen;
|
||||
use App\Models\Pertanian\PendapatanKomoditas;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use App\Models\Profile;
|
||||
|
||||
use App\Enums\PraktikBudidaya\FrekuensiPemangkasanUtama;
|
||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
|
||||
use App\Enums\PraktikBudidaya\FrekuensiSanitasiLahan;
|
||||
use App\Enums\PraktikBudidaya\PenangananGulma;
|
||||
use App\Enums\PraktikBudidaya\KondisiPengelolaanAir;
|
||||
use App\Enums\SosialEkonomi\SkorKecukupanPenghasilan;
|
||||
use App\Enums\SosialEkonomi\SkorKemampuanMenabung;
|
||||
use App\Enums\SosialEkonomi\TerdaftarKelompokTani;
|
||||
use App\Enums\SosialEkonomi\PeranDalamKelompok;
|
||||
use App\Enums\SosialEkonomi\StatusRekeningTabungan;
|
||||
use App\Enums\SosialEkonomi\PengalamanKreditKebun;
|
||||
use App\Enums\SosialEkonomi\KeterikatanHutangPanen;
|
||||
use App\Enums\SosialEkonomi\PraktikIjon;
|
||||
use App\Enums\SosialEkonomi\KebebasanMemilihPembeli;
|
||||
|
||||
class ProfilPetaniController extends Controller
|
||||
{
|
||||
private const DIMENSION_LOW_MAX = 2.49;
|
||||
private const DIMENSION_MEDIUM_MAX = 3.49;
|
||||
|
||||
public function index(int $id)
|
||||
{
|
||||
$petani = Petani::findOrFail($id);
|
||||
$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::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) : '-';
|
||||
$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?->updated_at && $tanaman->umur_tanaman !== null) {
|
||||
$tahunInputTanaman = $tanaman->updated_at->year;
|
||||
$umurTanaman = (int) $tanaman->umur_tanaman;
|
||||
$tahunSekarang = now()->year;
|
||||
|
||||
$tahunTanam = $tahunInputTanaman - $umurTanaman;
|
||||
$umurTanaman = $tahunSekarang - $tahunTanam;
|
||||
} else {
|
||||
$tahunTanam = null;
|
||||
$umurTanaman = null;
|
||||
}
|
||||
|
||||
$praktikBudidaya = PraktikBudidaya::where('petani_id', $petani->id)->first();
|
||||
$surveyPanen = SurveyPanen::where('petani_id', $petani->id)->first();
|
||||
$sosialEkonomi = SosialEkonomi::where('petani_id', $id)->first();
|
||||
|
||||
$indikatorOnFarm = collect();
|
||||
|
||||
if ($praktikBudidaya) {
|
||||
|
||||
$indikatorOnFarm = collect([
|
||||
[
|
||||
'key' => 'pemangkasan',
|
||||
'nama' => 'Pemangkasan',
|
||||
'skor' => $praktikBudidaya->frekuensi_pemangkasan_utama,
|
||||
'deskripsi' => FrekuensiPemangkasanUtama::tryFrom(
|
||||
(int) $praktikBudidaya->frekuensi_pemangkasan_utama
|
||||
)?->label(),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'pemupukan',
|
||||
'nama' => 'Pemupukan',
|
||||
'skor' => $praktikBudidaya->pemberian_pupuk,
|
||||
'deskripsi' => PemberianPupuk::tryFrom(
|
||||
(int) $praktikBudidaya->pemberian_pupuk
|
||||
)?->label(),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'opt',
|
||||
'nama' => 'Hama / Penyakit',
|
||||
'skor' => $praktikBudidaya->metode_penanganan_opt,
|
||||
'deskripsi' => MetodePenangananHamaPenyakit::tryFrom(
|
||||
(int) $praktikBudidaya->metode_penanganan_opt
|
||||
)?->label(),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'sanitasi',
|
||||
'nama' => 'Sanitasi Kebun',
|
||||
'skor' => $praktikBudidaya->frekuensi_sanitasi_lahan,
|
||||
'deskripsi' => FrekuensiSanitasiLahan::tryFrom(
|
||||
(int) $praktikBudidaya->frekuensi_sanitasi_lahan
|
||||
)?->label(),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'gulma',
|
||||
'nama' => 'Penanganan Gulma',
|
||||
'skor' => $praktikBudidaya->penanganan_gulma,
|
||||
'deskripsi' => PenangananGulma::tryFrom(
|
||||
(int) $praktikBudidaya->penanganan_gulma
|
||||
)?->label(),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'air',
|
||||
'nama' => 'Pengelolaan Air',
|
||||
'skor' => $praktikBudidaya->kondisi_pengelolaan_air,
|
||||
'deskripsi' => KondisiPengelolaanAir::tryFrom(
|
||||
(int) $praktikBudidaya->kondisi_pengelolaan_air
|
||||
)?->label(),
|
||||
],
|
||||
])
|
||||
->map(function ($item) {
|
||||
|
||||
$skor = $item['skor'] !== null
|
||||
? (int) $item['skor']
|
||||
: null;
|
||||
|
||||
return array_merge($item, [
|
||||
'skor' => $skor,
|
||||
'persentase' => $skor !== null
|
||||
? ($skor / 5) * 100
|
||||
: 0,
|
||||
|
||||
'color' => $this->getScoreColor($skor),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
$detailOnFarm = [
|
||||
'jenis_hama' => $praktikBudidaya?->jenis_hama ?? [],
|
||||
'jenis_penyakit' => $praktikBudidaya?->jenis_penyakit ?? [],
|
||||
'konservasi_tanah' => $praktikBudidaya?->kebiasaan_konservasi_tanah ?? [],
|
||||
];
|
||||
|
||||
$d4SosialEkonomi = null;
|
||||
|
||||
$terdaftarKelompok = $sosialEkonomi?->terdaftar_kelompok_tani;
|
||||
$peranKelompok = $sosialEkonomi?->peran_dalam_kelompok;
|
||||
|
||||
if ($sosialEkonomi) {
|
||||
|
||||
$kecukupanPenghasilan = $sosialEkonomi->skor_kecukupan_penghasilan;
|
||||
$kemampuanMenabung = $sosialEkonomi->skor_kemampuan_menabung;
|
||||
|
||||
$rekening = $sosialEkonomi->status_rekening_tabungan;
|
||||
$kredit = $sosialEkonomi->pengalaman_kredit_kebun;
|
||||
$hutangPanen = $sosialEkonomi->keterikatan_hutang_panen;
|
||||
$ijon = $sosialEkonomi->praktik_ijon;
|
||||
|
||||
$programKeuanganRaw = $sosialEkonomi->status_program_keuangan_formal ?? [];
|
||||
$namaProgram = [
|
||||
'pkh' => 'PKH',
|
||||
'blt' => 'BLT',
|
||||
'kur' => 'KUR',
|
||||
'kredit_koperasi_resmi' => 'Kredit Koperasi',
|
||||
'credit_union_cu' => 'Credit Union (CU)',
|
||||
'pinjaman_bumdes' => 'Pinjaman BUMDes',
|
||||
'modal_hibah_dinas_pertanian' => 'Hibah Dinas Pertanian',
|
||||
];
|
||||
|
||||
$programBantuan = collect($programKeuanganRaw)->map(function ($status, $key) use ($namaProgram) {
|
||||
$statusMeta = match ($status) {
|
||||
'sedang_menerima' => [
|
||||
'label' => 'Sedang Menerima',
|
||||
'short' => 'Aktif',
|
||||
'color' => 'success',
|
||||
'icon' => 'tabler-circle-check',
|
||||
],
|
||||
'pernah_tapi_tidak_lagi' => [
|
||||
'label' => 'Pernah, Tidak Lagi',
|
||||
'short' => 'Pernah',
|
||||
'color' => 'info',
|
||||
'icon' => 'tabler-history',
|
||||
],
|
||||
'belum_pernah' => [
|
||||
'label' => 'Belum Pernah',
|
||||
'short' => 'Belum Pernah',
|
||||
'color' => 'secondary',
|
||||
'icon' => 'tabler-minus',
|
||||
],
|
||||
default => [
|
||||
'label' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||
'short' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||
'color' => 'secondary',
|
||||
'icon' => 'tabler-help-circle',
|
||||
],
|
||||
};
|
||||
|
||||
return [
|
||||
'key' => $key,
|
||||
'nama' => $namaProgram[$key] ?? Str::headline($key),
|
||||
'status' => $status,
|
||||
'label' => $statusMeta['label'],
|
||||
'short' => $statusMeta['short'],
|
||||
'color' => $statusMeta['color'],
|
||||
'icon' => $statusMeta['icon'],
|
||||
];
|
||||
|
||||
})
|
||||
->values();
|
||||
|
||||
$d4SosialEkonomi = [
|
||||
|
||||
'jumlah_tanggungan' => $sosialEkonomi->jumlah_tanggungan,
|
||||
|
||||
'kecukupan_penghasilan' => [
|
||||
'skor' => $kecukupanPenghasilan?->value,
|
||||
'label' => $kecukupanPenghasilan?->label(),
|
||||
|
||||
'color' => $this->getScoreColor(
|
||||
$kecukupanPenghasilan?->value
|
||||
),
|
||||
],
|
||||
|
||||
'kemampuan_menabung' => [
|
||||
'skor' => $kemampuanMenabung?->value,
|
||||
'label' => $kemampuanMenabung?->label(),
|
||||
|
||||
'color' => $this->getScoreColor(
|
||||
$kemampuanMenabung?->value
|
||||
),
|
||||
],
|
||||
|
||||
'status_kepemilikan_lahan' => $sosialEkonomi->status_kepemilikan_lahan,
|
||||
|
||||
'kelembagaan' => [
|
||||
'terdaftar' =>
|
||||
$terdaftarKelompok === TerdaftarKelompokTani::YA,
|
||||
|
||||
'status' =>
|
||||
$terdaftarKelompok?->label(),
|
||||
|
||||
'peran' =>
|
||||
$peranKelompok?->label(),
|
||||
],
|
||||
|
||||
'kondisi_keuangan' => [
|
||||
'rekening' => [
|
||||
'label' => $rekening?->label(),
|
||||
'value' => $rekening?->value,
|
||||
'color' => $this->getFinancialColor(
|
||||
$rekening?->value
|
||||
),
|
||||
'icon' => 'tabler-building-bank',
|
||||
],
|
||||
|
||||
'kredit' => [
|
||||
'label' => $kredit?->label(),
|
||||
'value' => $kredit?->value,
|
||||
'color' => $this->getFinancialColor(
|
||||
$kredit?->value
|
||||
),
|
||||
'icon' => 'tabler-credit-card',
|
||||
],
|
||||
|
||||
'hutang_panen' => [
|
||||
'label' => $hutangPanen?->label(),
|
||||
'value' => $hutangPanen?->value,
|
||||
'color' => $this->getFinancialColor(
|
||||
$hutangPanen?->value
|
||||
),
|
||||
'icon' => 'tabler-coins',
|
||||
],
|
||||
|
||||
'ijon' => [
|
||||
'label' => $ijon?->label(),
|
||||
'value' => $ijon?->value,
|
||||
'color' => $this->getFinancialColor(
|
||||
$ijon?->value
|
||||
),
|
||||
'icon' => 'tabler-link',
|
||||
],
|
||||
],
|
||||
|
||||
'program_bantuan' => $programBantuan,
|
||||
];
|
||||
}
|
||||
|
||||
$surveyPelatihan = SurveyPelatihan::where('petani_id', $id)
|
||||
->orderByDesc('tahun_pelaksanaan')
|
||||
->get();
|
||||
|
||||
$surveySertifikasi = SurveySertifikasi::where('petani_id', $id)
|
||||
->orderByDesc('tahun_pelaksanaan')
|
||||
->get();
|
||||
|
||||
$aksesPasar = AksesPasar::where('petani_id', $id)->first();
|
||||
$saluranTerdahulu = collect($aksesPasar?->saluran_terdahulu ?? [] );
|
||||
|
||||
$d6SaluranPasar = [
|
||||
[
|
||||
'key' => 'tengkulak',
|
||||
'label' => 'Tengkulak',
|
||||
'icon' => 'tabler-user-dollar',
|
||||
'aktif' => $saluranTerdahulu->contains(
|
||||
fn ($item) => Str::contains(
|
||||
Str::lower($item),
|
||||
['tengkulak', 'pedagang lokal']
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'koperasi',
|
||||
'label' => 'Koperasi',
|
||||
'icon' => 'tabler-users-group',
|
||||
'aktif' => $saluranTerdahulu->contains(
|
||||
fn ($item) => Str::contains(
|
||||
Str::lower($item),
|
||||
'koperasi'
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'bumdes',
|
||||
'label' => 'BUMDes',
|
||||
'icon' => 'tabler-building-community',
|
||||
'aktif' => $saluranTerdahulu->contains(
|
||||
fn ($item) => Str::contains(
|
||||
Str::lower($item),
|
||||
'bumdes'
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'eksportir',
|
||||
'label' => 'Eksportir',
|
||||
'icon' => 'tabler-plane-departure',
|
||||
'aktif' => $saluranTerdahulu->contains(
|
||||
fn ($item) => Str::contains(
|
||||
Str::lower($item),
|
||||
'eksportir'
|
||||
)
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
$hargaJualPanen = HargaJualPanen::where('petani_id', $id)->get();
|
||||
|
||||
$d6HargaJual = $hargaJualPanen->map(function ($item) {
|
||||
|
||||
$hargaSekarang = $item->harga_panen_terakhir !== null
|
||||
? (float) $item->harga_panen_terakhir
|
||||
: null;
|
||||
|
||||
$hargaTahunLalu = $item->harga_tahun_lalu !== null
|
||||
? (float) $item->harga_tahun_lalu
|
||||
: null;
|
||||
|
||||
$perubahan = null;
|
||||
|
||||
if (
|
||||
$hargaSekarang !== null &&
|
||||
$hargaTahunLalu !== null &&
|
||||
$hargaTahunLalu > 0
|
||||
) {
|
||||
$perubahan = (
|
||||
($hargaSekarang - $hargaTahunLalu)
|
||||
/ $hargaTahunLalu
|
||||
) * 100;
|
||||
}
|
||||
|
||||
return [
|
||||
'bentuk_produk' => $item->bentuk_produk,
|
||||
|
||||
'harga_sekarang' => $hargaSekarang,
|
||||
|
||||
'harga_tahun_lalu' => $hargaTahunLalu,
|
||||
|
||||
'harga_dua_tahun_lalu' =>
|
||||
$item->harga_dua_tahun_lalu !== null
|
||||
? (float) $item->harga_dua_tahun_lalu
|
||||
: null,
|
||||
|
||||
'perubahan' => $perubahan,
|
||||
|
||||
'trend' => match (true) {
|
||||
$perubahan === null => 'neutral',
|
||||
$perubahan > 0 => 'up',
|
||||
$perubahan < 0 => 'down',
|
||||
default => 'stable',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
$kebebasanPembeli = $sosialEkonomi?->kebebasan_memilih_pembeli;
|
||||
|
||||
$d6RantaiNilai = [
|
||||
'saluran_pasar' => $d6SaluranPasar,
|
||||
|
||||
'harga_jual' => $d6HargaJual,
|
||||
|
||||
'kebebasan_memilih_pembeli' => [
|
||||
'skor' => $kebebasanPembeli?->value,
|
||||
'label' => $kebebasanPembeli?->label(),
|
||||
|
||||
'persentase' => $kebebasanPembeli
|
||||
? ($kebebasanPembeli->value / 5) * 100
|
||||
: 0,
|
||||
|
||||
'color' => $this->getScoreColor(
|
||||
$kebebasanPembeli?->value
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
// 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,
|
||||
'tahunTanam' => $tahunTanam,
|
||||
'umurTanaman' => $umurTanaman,
|
||||
'surveyPanen' => $surveyPanen,
|
||||
'praktikBudidaya' => $praktikBudidaya,
|
||||
'indikatorOnFarm' => $indikatorOnFarm,
|
||||
'detailOnFarm' => $detailOnFarm,
|
||||
'sosialEkonomi' => $sosialEkonomi,
|
||||
'd4SosialEkonomi' => $d4SosialEkonomi,
|
||||
'surveyPelatihan' => $surveyPelatihan,
|
||||
'surveySertifikasi' => $surveySertifikasi,
|
||||
'aksesPasar' => $aksesPasar,
|
||||
'd6SaluranPasar' => $d6SaluranPasar,
|
||||
'hargaJualPanen' => $hargaJualPanen,
|
||||
'd6HargaJual' => $d6HargaJual,
|
||||
'kebebasanPembeli' => $kebebasanPembeli,
|
||||
'd6RantaiNilai' => $d6RantaiNilai
|
||||
];
|
||||
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) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$score >= 5 => 'success',
|
||||
$score >= 4 => 'info',
|
||||
$score >= 3 => 'primary',
|
||||
$score >= 2 => 'warning',
|
||||
default => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
private function getFinancialColor(?int $score): string
|
||||
{
|
||||
if ($score === null || $score === 0) {
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$score >= 5 => 'success',
|
||||
$score >= 4 => 'info',
|
||||
$score >= 2 => 'warning',
|
||||
default => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildProfiling(SkorPetani $skorPetani): array
|
||||
{
|
||||
$skorAkhir = $this->normalizeScore($skorPetani->skor_kesiapan_adopsi);
|
||||
$segmen = $this->getSegmen($skorAkhir);
|
||||
|
||||
$dimensions = [
|
||||
'd2' => [
|
||||
'kode' => 'D2',
|
||||
'nama' => 'Sistem & Praktik Budidaya',
|
||||
'skor' => $this->normalizeScore(
|
||||
$skorPetani->skor_d2_sistem_praktik_budidaya
|
||||
),
|
||||
],
|
||||
|
||||
'd4' => [
|
||||
'kode' => 'D4',
|
||||
'nama' => 'Sosial-Ekonomi Rumah Tangga',
|
||||
'skor' => $this->normalizeScore(
|
||||
$skorPetani->skor_d4_sosial_ekonomi
|
||||
),
|
||||
],
|
||||
|
||||
'd6' => [
|
||||
'kode' => 'D6',
|
||||
'nama' => 'Rantai Nilai & Akses Pasar',
|
||||
'skor' => $this->normalizeScore(
|
||||
$skorPetani->skor_d6_rantai_nilai_akses_pasar
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($dimensions as $key => $dimension) {
|
||||
$dimensions[$key]['level'] = $this->getDimensionLevel($dimension['skor']);
|
||||
}
|
||||
|
||||
$components = collect([
|
||||
[
|
||||
'key' => 'motivasi',
|
||||
'nama' => 'Motivasi & Kapasitas Adopsi',
|
||||
'short' => 'Motivasi',
|
||||
'dimensi' => 'D4',
|
||||
'bobot' => 25,
|
||||
'icon' => 'tabler-bulb',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'ekonomi',
|
||||
'nama' => 'Kondisi Ekonomi Rumah Tangga',
|
||||
'short' => 'Ekonomi',
|
||||
'dimensi' => 'D4',
|
||||
'bobot' => 20,
|
||||
'icon' => 'tabler-home-dollar',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_2_d4_k2_ekonomi_rumah_tangga),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'gap',
|
||||
'nama' => 'Praktik Budidaya & GAP',
|
||||
'short' => 'Budidaya / GAP',
|
||||
'dimensi' => 'D2',
|
||||
'bobot' => 20,
|
||||
'icon' => 'tabler-plant-2',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_3_d2_praktik_budidaya_gap),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'kelembagaan',
|
||||
'nama' => 'Keterlibatan Kelembagaan',
|
||||
'short' => 'Kelembagaan',
|
||||
'dimensi' => 'D4',
|
||||
'bobot' => 15,
|
||||
'icon' => 'tabler-users-group',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_4_d4_k4_keterlibatan_kelembagaan),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'keuangan',
|
||||
'nama' => 'Akses & Kebebasan Keuangan',
|
||||
'short' => 'Keuangan',
|
||||
'dimensi' => 'D4',
|
||||
'bobot' => 10,
|
||||
'icon' => 'tabler-building-bank',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan),
|
||||
],
|
||||
|
||||
[
|
||||
'key' => 'pasar',
|
||||
'nama' => 'Rantai Nilai & Akses Pasar',
|
||||
'short' => 'Akses Pasar',
|
||||
'dimensi' => 'D6',
|
||||
'bobot' => 10,
|
||||
'icon' => 'tabler-building-store',
|
||||
'skor' => $this->normalizeScore($skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar),
|
||||
],
|
||||
]);
|
||||
|
||||
$availableComponents = $components
|
||||
->filter(fn ($item) => $item['skor'] !== null)
|
||||
->values();
|
||||
|
||||
$strengths = $availableComponents
|
||||
->sortByDesc('skor')
|
||||
->take(2)
|
||||
->values();
|
||||
|
||||
$barriers = $availableComponents
|
||||
->filter(fn ($item) => $item['skor'] < 3)
|
||||
->sortBy('skor')
|
||||
->values();
|
||||
|
||||
$specialPattern = $this->detectSpecialPattern(
|
||||
$segmen,
|
||||
$dimensions,
|
||||
$skorPetani
|
||||
);
|
||||
|
||||
$title = $this->buildProfileTitle(
|
||||
$segmen,
|
||||
$specialPattern,
|
||||
$barriers
|
||||
);
|
||||
|
||||
$narrative = $this->buildNarrative(
|
||||
$segmen,
|
||||
$specialPattern,
|
||||
$strengths,
|
||||
$barriers
|
||||
);
|
||||
|
||||
$recommendations = $this->buildRecommendations(
|
||||
$specialPattern,
|
||||
$barriers
|
||||
);
|
||||
|
||||
return [
|
||||
'skor_akhir' => $skorAkhir,
|
||||
'segmen' => $segmen,
|
||||
'title' => $title,
|
||||
'dimensions' => $dimensions,
|
||||
'components' => $components,
|
||||
'strengths' => $strengths,
|
||||
'barriers' => $barriers,
|
||||
'special_pattern' => $specialPattern,
|
||||
'narrative' => $narrative,
|
||||
'recommendations' => $recommendations,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeScore($score): ?float
|
||||
{
|
||||
if ($score === null || $score === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$score = (float) $score;
|
||||
|
||||
if ($score <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
private function getSegmen(?float $score): string
|
||||
{
|
||||
if ($score === null) {
|
||||
return 'Belum Dinilai';
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$score >= 4.0 => 'Penggerak',
|
||||
$score >= 3.0 => 'Perintis',
|
||||
$score >= 2.0 => 'Pemerhati',
|
||||
default => 'Pemula',
|
||||
};
|
||||
}
|
||||
|
||||
private function getDimensionLevel(?float $score): string
|
||||
{
|
||||
if ($score === null) {
|
||||
return 'belum_dinilai';
|
||||
}
|
||||
|
||||
if ($score <= self::DIMENSION_LOW_MAX) {
|
||||
return 'rendah';
|
||||
}
|
||||
|
||||
if ($score <= self::DIMENSION_MEDIUM_MAX) {
|
||||
return 'sedang';
|
||||
}
|
||||
|
||||
return 'tinggi';
|
||||
}
|
||||
|
||||
private function detectSpecialPattern(
|
||||
string $segmen,
|
||||
array $dimensions,
|
||||
SkorPetani $skorPetani
|
||||
): ?array {
|
||||
|
||||
$d2 = $dimensions['d2']['level'];
|
||||
$d4 = $dimensions['d4']['level'];
|
||||
$d6 = $dimensions['d6']['level'];
|
||||
|
||||
if (
|
||||
$segmen === 'Perintis' &&
|
||||
$d4 === 'tinggi' &&
|
||||
$d2 === 'rendah' &&
|
||||
$d6 === 'sedang'
|
||||
) {
|
||||
return [
|
||||
'kode' => 'perintis_teknis',
|
||||
'nama' => 'Perintis dengan Hambatan Teknis',
|
||||
'prioritas' => 'gap',
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
$segmen === 'Perintis' &&
|
||||
$d4 === 'tinggi' &&
|
||||
in_array($d2, ['sedang', 'tinggi']) && $d6 === 'rendah'
|
||||
) {
|
||||
return [
|
||||
'kode' => 'perintis_pasar',
|
||||
'nama' => 'Perintis dengan Hambatan Pasar',
|
||||
'prioritas' => 'pasar',
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
$segmen === 'Pemerhati' &&
|
||||
$d4 === 'sedang' &&
|
||||
$d2 === 'rendah' &&
|
||||
$d6 === 'sedang'
|
||||
) {
|
||||
return [
|
||||
'kode' => 'pemerhati_teknis',
|
||||
'nama' => 'Pemerhati Teknis',
|
||||
'prioritas' => 'gap',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
$segmen === 'Pemula' &&
|
||||
$d4 === 'rendah' &&
|
||||
$d2 === 'rendah' &&
|
||||
$d6 === 'rendah'
|
||||
) {
|
||||
return [
|
||||
'kode' => 'pemula_struktural',
|
||||
'nama' => 'Pemula Struktural',
|
||||
'prioritas' => 'struktural',
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function buildProfileTitle(
|
||||
string $segmen,
|
||||
?array $specialPattern,
|
||||
$barriers
|
||||
): string {
|
||||
if ($specialPattern) {
|
||||
return $specialPattern['nama'];
|
||||
}
|
||||
|
||||
if ($barriers->isNotEmpty()) {
|
||||
$mainBarrier = $barriers->first();
|
||||
return $segmen . ' dengan Prioritas ' . $mainBarrier['short'];
|
||||
}
|
||||
|
||||
return $segmen . ' dengan Profil Relatif Seimbang';
|
||||
}
|
||||
|
||||
private function buildNarrative(
|
||||
string $segmen,
|
||||
?array $specialPattern,
|
||||
$strengths,
|
||||
$barriers
|
||||
): string {
|
||||
if ($specialPattern) {
|
||||
return match ($specialPattern['kode']) {
|
||||
'perintis_teknis' =>
|
||||
'Petani memiliki fondasi sosial-ekonomi yang relatif siap '
|
||||
. 'untuk berkembang, tetapi penerapannya belum sepenuhnya '
|
||||
. 'tercermin dalam praktik budidaya. Hambatan utama berada '
|
||||
. 'pada aspek teknis di lapangan.',
|
||||
|
||||
'perintis_pasar' =>
|
||||
'Petani memiliki fondasi sosial-ekonomi dan kesiapan '
|
||||
. 'budidaya yang relatif baik, tetapi manfaat ekonominya '
|
||||
. 'masih dibatasi oleh rantai nilai dan akses pasar.',
|
||||
|
||||
'pemerhati_teknis' =>
|
||||
'Petani telah memiliki sebagian fondasi untuk berkembang, '
|
||||
. 'namun praktik budidaya masih menjadi hambatan utama. '
|
||||
. 'Pendekatan berbasis demonstrasi hasil nyata akan lebih '
|
||||
. 'relevan sebelum intervensi teknis yang lebih lanjut.',
|
||||
|
||||
'pemula_struktural' =>
|
||||
'Petani menghadapi hambatan pada beberapa dimensi sekaligus. '
|
||||
. 'Kondisi sosial-ekonomi, praktik budidaya, serta akses '
|
||||
. 'pasar perlu dibaca bersama untuk menentukan hambatan '
|
||||
. 'mendasar yang paling mendesak.',
|
||||
|
||||
default =>
|
||||
$this->buildGenericNarrative($segmen, $strengths, $barriers),
|
||||
};
|
||||
}
|
||||
|
||||
return $this->buildGenericNarrative($segmen, $strengths, $barriers);
|
||||
}
|
||||
|
||||
private function buildGenericNarrative(
|
||||
string $segmen,
|
||||
$strengths,
|
||||
$barriers
|
||||
): string {
|
||||
|
||||
$text = 'Petani berada pada segmen ' . $segmen . '. ';
|
||||
if ($strengths->isNotEmpty()) {
|
||||
$strengthNames = $strengths->pluck('nama')->implode(' dan ');
|
||||
$text .= 'Kekuatan relatif terlihat pada ' . $strengthNames . '. ';
|
||||
}
|
||||
|
||||
if ($barriers->isNotEmpty()) {
|
||||
$barrierNames = $barriers->take(3)->pluck('nama')->implode(', ');
|
||||
$text .=
|
||||
'Area yang masih memerlukan perhatian terutama pada ' . $barrierNames . '. Prioritas intervensi sebaiknya mengikuti ' . 'hambatan dengan skor paling rendah.';
|
||||
} else {
|
||||
$text .= 'Tidak terdapat komponen dengan skor rendah yang ' . 'menonjol. Pendampingan dapat difokuskan pada ' . 'pemeliharaan dan penguatan capaian yang sudah ada.';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function buildRecommendations(
|
||||
?array $specialPattern,
|
||||
$barriers
|
||||
): array {
|
||||
if ($specialPattern) {
|
||||
$specialRecommendation = match ($specialPattern['kode']) {
|
||||
'perintis_teknis' => [
|
||||
'Prioritaskan pendampingan GAP langsung di lapangan.',
|
||||
'Perkuat koneksi petani terhadap sumber input dan pelatihan teknis.',
|
||||
],
|
||||
'perintis_pasar' => [
|
||||
'Perkuat koneksi ke saluran pasar yang lebih baik.',
|
||||
'Fasilitasi akses pembeli, negosiasi, atau penguatan koperasi.',
|
||||
],
|
||||
'pemerhati_teknis' => [
|
||||
'Gunakan demonstrasi lapangan untuk menunjukkan hasil nyata.',
|
||||
'Lakukan pendampingan teknis secara bertahap sebelum pelatihan formal.',
|
||||
],
|
||||
'pemula_struktural' => [
|
||||
'Identifikasi hambatan dasar seperti lahan, hutang, atau logistik.',
|
||||
'Tangani hambatan struktural sebelum mendorong adopsi praktik baru.',
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
if (!empty($specialRecommendation)) {
|
||||
return $specialRecommendation;
|
||||
}
|
||||
}
|
||||
$recommendationMap = [
|
||||
'motivasi' => 'Gunakan demonstrasi hasil nyata, role model, dan pendekatan perubahan secara bertahap.',
|
||||
'ekonomi' => 'Perkuat ruang gerak ekonomi rumah tangga sebelum mendorong investasi atau perubahan praktik yang berisiko.',
|
||||
'gap' => 'Prioritaskan pendampingan praktik budidaya dan GAP langsung di kebun.',
|
||||
'kelembagaan' => 'Perkuat keterlibatan petani dalam kelompok tani sebagai ruang belajar dan penguatan posisi tawar.',
|
||||
'keuangan' => 'Tinjau akses keuangan formal serta kemungkinan ikatan hutang panen atau ijon yang membatasi ruang gerak petani.',
|
||||
'pasar' => 'Perkuat koneksi dengan pembeli dan saluran pasar alternatif serta kemampuan negosiasi petani.',
|
||||
];
|
||||
|
||||
return $barriers->take(3)->map(
|
||||
fn ($item) =>
|
||||
$recommendationMap[$item['key']] ?? null
|
||||
)->filter()->values()->all();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,26 @@ class ProfileController extends Controller
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'telepon' => ['required', 'max:13'],
|
||||
'desa_kelurahan_id' => ['required'],
|
||||
'telepon' => ['nullable', 'max:13'],
|
||||
'desa_kelurahan_id' => ['nullable', 'exists:master_desa_kelurahan,id'],
|
||||
'ktp' => ['nullable', 'min:16', 'max:16'],
|
||||
'file_ktp' => ['nullable', 'file', 'mimes:jpeg,jpg,png,pdf', 'max:2048'],
|
||||
'kk' => ['nullable', 'min:16', 'max:16'],
|
||||
'file_kk' => ['nullable', 'file', 'mimes:jpeg,jpg,png,pdf', 'max:2048'],
|
||||
], [
|
||||
'nama.required' => 'Nama wajib diisi.',
|
||||
'email.required' => 'Email wajib diisi.',
|
||||
'email.email' => 'Format email tidak sesuai.',
|
||||
'telepon.max' => 'Nomor telepon tidak sesuai.',
|
||||
'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.',
|
||||
'ktp.min' => 'KTP harus terdiri dari 16 digit.',
|
||||
'ktp.max' => 'KTP harus terdiri dari 16 digit.',
|
||||
'kk.min' => 'KK harus terdiri dari 16 digit.',
|
||||
'kk.max' => 'KK harus terdiri dari 16 digit.',
|
||||
'file_ktp.mimes' => 'Format file harus dalam bentuk JPEG, JPG, PNG, atau PDF.',
|
||||
'file_kk.mimes' => 'Format file harus dalam bentuk JPEG, JPG, PNG, atau PDF.',
|
||||
'file_ktp.max' => 'Ukuran file maksimal 2MB.',
|
||||
'file_kk.max' => 'Ukuran file maksimal 2MB.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -143,12 +157,31 @@ class ProfileController extends Controller
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'telepon' => ['required', 'max:13'],
|
||||
'desa_kelurahan_id' => ['required'],
|
||||
'telepon' => ['nullable', 'max:13'],
|
||||
'desa_kelurahan_id' => ['nullable', 'exists:master_desa_kelurahan,id'],
|
||||
'ktp' => ['nullable', 'min:16', 'max:16'],
|
||||
'file_ktp' => ['nullable', 'file', 'mimes:jpeg,jpg,png,pdf', 'max:2048'],
|
||||
'kk' => ['nullable', 'min:16', 'max:16'],
|
||||
'file_kk' => ['nullable', 'file', 'mimes:jpeg,jpg,png,pdf', 'max:2048'],
|
||||
], [
|
||||
'nama.required' => 'Nama harus diisi.',
|
||||
'nama.string' => 'Nama harus berupa string.',
|
||||
'nama.max' => 'Nama tidak boleh lebih dari 255 karakter.',
|
||||
'email.required' => 'Email harus diisi.',
|
||||
'email.email' => 'Email harus berupa alamat email yang valid.',
|
||||
'email.max' => 'Email tidak boleh lebih dari 255 karakter.',
|
||||
'telepon.max' => 'Nomor telepon tidak boleh lebih dari 13 karakter.',
|
||||
'desa_kelurahan_id.exists' => 'Desa kelurahan tidak ditemukan.',
|
||||
'ktp.min' => 'Nomor KTP harus terdiri dari 16 karakter.',
|
||||
'ktp.max' => 'Nomor KTP harus terdiri dari 16 karakter.',
|
||||
'kk.min' => 'Nomor KK harus terdiri dari 16 karakter.',
|
||||
'kk.max' => 'Nomor KK harus terdiri dari 16 karakter.',
|
||||
'file_ktp.file' => 'File harus berupa file.',
|
||||
'file_ktp.mimes' => 'File harus berupa file dengan ekstensi jpeg, jpg, png, atau pdf.',
|
||||
'file_ktp.max' => 'Ukuran file tidak boleh lebih dari 2MB.',
|
||||
'file_kk.file' => 'File harus berupa file.',
|
||||
'file_kk.mimes' => 'File harus berupa file dengan ekstensi jpeg, jpg, png, atau pdf.',
|
||||
'file_kk.max' => 'Ukuran file tidak boleh lebih dari 2MB.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -154,8 +154,7 @@ class SosialEkonomiController extends Controller
|
||||
});
|
||||
|
||||
if (!$request->expectsJson()) {
|
||||
return redirect()
|
||||
->route('petani.sosial-ekonomi.edit', [$sosialEkonomi->petani_id, $sosialEkonomi->id])
|
||||
return $this->redirectToEditSection($sosialEkonomi, $request)
|
||||
->with('success', 'Data sosial ekonomi berhasil disimpan.');
|
||||
}
|
||||
|
||||
@@ -206,8 +205,7 @@ class SosialEkonomiController extends Controller
|
||||
});
|
||||
|
||||
if (!$request->expectsJson()) {
|
||||
return redirect()
|
||||
->route('petani.sosial-ekonomi.edit', [$sosialEkonomi->petani_id, $sosialEkonomi->id])
|
||||
return $this->redirectToEditSection($sosialEkonomi, $request)
|
||||
->with('success', 'Data sosial ekonomi berhasil diperbarui.');
|
||||
}
|
||||
|
||||
@@ -218,6 +216,18 @@ class SosialEkonomiController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function redirectToEditSection(SosialEkonomi $sosialEkonomi, Request $request): RedirectResponse
|
||||
{
|
||||
$url = route('petani.sosial-ekonomi.edit', [$sosialEkonomi->petani_id, $sosialEkonomi->id]);
|
||||
$fragment = $request->input('redirect_fragment');
|
||||
|
||||
if (is_string($fragment) && preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
|
||||
$url .= '#' . $fragment;
|
||||
}
|
||||
|
||||
return redirect()->to($url);
|
||||
}
|
||||
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
$sosialEkonomi = SosialEkonomi::findOrFail($id);
|
||||
@@ -388,15 +398,17 @@ class SosialEkonomiController extends Controller
|
||||
}
|
||||
|
||||
if ($section === 'keanggotaan_kelompok') {
|
||||
$terdaftarKelompokTani = $request->input('terdaftar_kelompok_tani') === TerdaftarKelompokTani::YA->value;
|
||||
|
||||
$rules = array_merge($rules, [
|
||||
'terdaftar_kelompok_tani' => ['nullable', 'string', Rule::in(TerdaftarKelompokTani::values())],
|
||||
'nama_kelompok_tani' => ['required_if:terdaftar_kelompok_tani,' . TerdaftarKelompokTani::YA->value, 'nullable', 'string', 'max:255'],
|
||||
'lama_bergabung_kelompok_tahun' => ['required_if:terdaftar_kelompok_tani,' . TerdaftarKelompokTani::YA->value, 'nullable', 'integer', 'min:0'],
|
||||
'kedekatan_sesama_anggota' => ['required_if:terdaftar_kelompok_tani,' . TerdaftarKelompokTani::YA->value, 'nullable', 'string', Rule::in(KedekatanSesamaAnggota::values())],
|
||||
'keaktifan_pertemuan_kelompok' => ['required', 'integer', Rule::in(KeaktifanPertemuanKelompok::values())],
|
||||
'topik_biasa_dibahas' => ['required', 'string'],
|
||||
'peran_dalam_kelompok' => ['required', 'integer', Rule::in(PeranDalamKelompok::values())],
|
||||
'manfaat_kelompok_tani' => ['required', 'integer', Rule::in(ManfaatKelompokTani::values())],
|
||||
'keaktifan_pertemuan_kelompok' => [Rule::requiredIf($terdaftarKelompokTani), 'nullable', 'integer', Rule::in(KeaktifanPertemuanKelompok::values())],
|
||||
'topik_biasa_dibahas' => [Rule::requiredIf($terdaftarKelompokTani), 'nullable', 'string'],
|
||||
'peran_dalam_kelompok' => [Rule::requiredIf($terdaftarKelompokTani), 'nullable', 'integer', Rule::in(PeranDalamKelompok::values())],
|
||||
'manfaat_kelompok_tani' => [Rule::requiredIf($terdaftarKelompokTani), 'nullable', 'integer', Rule::in(ManfaatKelompokTani::values())],
|
||||
'status_kartu_tani' => ['nullable', 'string', Rule::in(StatusKartuTani::values())],
|
||||
'bisa_beli_pupuk_bersubsidi' => ['nullable', 'string', Rule::in(BisaBeliPupukBersubsidi::values())],
|
||||
'manfaat_lain_kartu_tani' => ['nullable', 'string'],
|
||||
@@ -589,6 +601,14 @@ class SosialEkonomiController extends Controller
|
||||
|
||||
$payload = $this->normalizeSkorPayload($payload);
|
||||
|
||||
if (array_key_exists('ada_sengketa', $payload) && !filter_var($payload['ada_sengketa'], FILTER_VALIDATE_BOOLEAN)) {
|
||||
$payload['keterangan_sengketa'] = null;
|
||||
$payload['pihak_membantu_konflik'] = null;
|
||||
$payload['pihak_membantu_konflik_nama'] = null;
|
||||
$payload['tahun_bantuan_konflik'] = null;
|
||||
$payload['status_konflik'] = null;
|
||||
}
|
||||
|
||||
if (($payload['pihak_membantu_konflik'] ?? null) === PihakMembantuKonflik::TIDAK_ADA_SELESAIKAN_SENDIRI->value) {
|
||||
$payload['pihak_membantu_konflik_nama'] = null;
|
||||
$payload['tahun_bantuan_konflik'] = null;
|
||||
@@ -615,6 +635,10 @@ class SosialEkonomiController extends Controller
|
||||
$payload['nama_kelompok_tani'] = null;
|
||||
$payload['lama_bergabung_kelompok_tahun'] = null;
|
||||
$payload['kedekatan_sesama_anggota'] = null;
|
||||
$payload['keaktifan_pertemuan_kelompok'] = KeaktifanPertemuanKelompok::TIDAK_PERNAH_HADIR->value;
|
||||
$payload['topik_biasa_dibahas'] = null;
|
||||
$payload['peran_dalam_kelompok'] = PeranDalamKelompok::ANGGOTA_PASIF->value;
|
||||
$payload['manfaat_kelompok_tani'] = ManfaatKelompokTani::TIDAK_ADA_MANFAAT->value;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -958,8 +982,11 @@ class SosialEkonomiController extends Controller
|
||||
'terdaftarKelompokTaniTidakValue' => TerdaftarKelompokTani::TIDAK->value,
|
||||
'kedekatanSesamaAnggotaOptions' => KedekatanSesamaAnggota::options(),
|
||||
'keaktifanPertemuanKelompokOptions' => KeaktifanPertemuanKelompok::options(),
|
||||
'keaktifanPertemuanKelompokLowestValue' => KeaktifanPertemuanKelompok::TIDAK_PERNAH_HADIR->value,
|
||||
'peranDalamKelompokOptions' => PeranDalamKelompok::options(),
|
||||
'peranDalamKelompokLowestValue' => PeranDalamKelompok::ANGGOTA_PASIF->value,
|
||||
'manfaatKelompokTaniOptions' => ManfaatKelompokTani::options(),
|
||||
'manfaatKelompokTaniLowestValue' => ManfaatKelompokTani::TIDAK_ADA_MANFAAT->value,
|
||||
'statusKartuTaniOptions' => StatusKartuTani::options(),
|
||||
'bisaBeliPupukBersubsidiOptions' => BisaBeliPupukBersubsidi::options(),
|
||||
'statusRekeningTabunganOptions' => StatusRekeningTabungan::options(),
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Produktivitas;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
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', 'enumerator'])) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||
{
|
||||
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
|
||||
'success' => $message,
|
||||
'success_section' => $section,
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'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'],
|
||||
'komoditas_id' => ['nullable', 'integer', 'exists:master_komoditas,id'],
|
||||
'jenis_kelompok' => ['nullable', 'string', 'max:50'],
|
||||
'sort_by' => ['nullable', 'string'],
|
||||
'per_page' => ['nullable', 'integer', 'in:10,25'],
|
||||
]);
|
||||
|
||||
$defaultKabupaten = KabupatenKota::select('id', 'nama')
|
||||
->whereRaw('lower(nama) like ?', ['%ngada%'])->orderBy('nama')->first();
|
||||
$allowedSorts = ['nama_asc', 'nama_desc', 'anggota_desc', 'anggota_asc'];
|
||||
$filters = [
|
||||
'search' => $validated['search'] ?? null,
|
||||
'kabupaten_kota_id' => $validated['kabupaten_kota_id'] ?? $defaultKabupaten?->id,
|
||||
'kecamatan_id' => $validated['kecamatan_id'] ?? null,
|
||||
'desa_kelurahan_id' => $validated['desa_kelurahan_id'] ?? null,
|
||||
'komoditas_id' => $validated['komoditas_id'] ?? null,
|
||||
'jenis_kelompok' => $validated['jenis_kelompok'] ?? null,
|
||||
'sort_by' => in_array($validated['sort_by'] ?? null, $allowedSorts, true)
|
||||
? $validated['sort_by'] : null,
|
||||
];
|
||||
|
||||
$query = KelompokTani::query()
|
||||
->when(filled($filters['search']), function ($query) use ($filters) {
|
||||
$search = '%' . Str::lower($filters['search']) . '%';
|
||||
$query->where(function ($query) use ($search) {
|
||||
$query->whereRaw('lower(nama_kelompok) like ?', [$search])
|
||||
->orWhereRaw('lower(nama_responden) like ?', [$search]);
|
||||
});
|
||||
})
|
||||
->when($filters['kabupaten_kota_id'], function ($query, $id) {
|
||||
$query->whereHas('desaKelurahan.kecamatan', fn ($query) => $query->where('kabupaten_kota_id', $id));
|
||||
})
|
||||
->when($filters['kecamatan_id'], function ($query, $id) {
|
||||
$query->whereHas('desaKelurahan', fn ($query) => $query->where('kecamatan_id', $id));
|
||||
})
|
||||
->when($filters['desa_kelurahan_id'], fn ($query, $id) => $query->where('desa_kelurahan_id', $id))
|
||||
->when($filters['komoditas_id'], function ($query, $id) {
|
||||
$query->where(function ($query) use ($id) {
|
||||
$query->whereJsonContains('komoditas_id', (string) $id)
|
||||
->orWhereJsonContains('komoditas_id', (int) $id);
|
||||
});
|
||||
})
|
||||
->when($filters['jenis_kelompok'], fn ($query, $jenis) => $query->where('jenis_kelompok', $jenis));
|
||||
|
||||
$summary = (clone $query)->selectRaw('
|
||||
COUNT(*) AS total_kelompok,
|
||||
COUNT(DISTINCT jenis_kelompok) AS total_jenis,
|
||||
COALESCE(SUM(jumlah_pengurus), 0) AS total_pengurus,
|
||||
COALESCE(SUM(jumlah_pengurus_aktif), 0) AS pengurus_aktif,
|
||||
COALESCE(SUM(jumlah_anggota_terdaftar), 0) AS total_anggota,
|
||||
COALESCE(SUM(jumlah_anggota_aktif), 0) AS anggota_aktif
|
||||
')->first();
|
||||
|
||||
match ($filters['sort_by']) {
|
||||
'nama_asc' => $query->orderBy('nama_kelompok'),
|
||||
'nama_desc' => $query->orderByDesc('nama_kelompok'),
|
||||
'anggota_desc' => $query->orderByRaw('COALESCE(jumlah_anggota_terdaftar, 0) DESC'),
|
||||
'anggota_asc' => $query->orderByRaw('COALESCE(jumlah_anggota_terdaftar, 0) ASC'),
|
||||
default => $query->latest(),
|
||||
};
|
||||
|
||||
$perPage = (int) ($validated['per_page'] ?? 10);
|
||||
$kelompokTani = $query->with('desaKelurahan.kecamatan')->orderByDesc('id')
|
||||
->paginate($perPage)->appends([...$filters, 'per_page' => $perPage]);
|
||||
$selectedKabupaten = $filters['kabupaten_kota_id']
|
||||
? KabupatenKota::select('id', 'nama')->find($filters['kabupaten_kota_id']) : null;
|
||||
$kecamatanOptions = $filters['kabupaten_kota_id']
|
||||
? Kecamatan::select('id', 'nama')->where('kabupaten_kota_id', $filters['kabupaten_kota_id'])->orderBy('nama')->get()
|
||||
: collect();
|
||||
$desaOptions = $filters['kecamatan_id']
|
||||
? DesaKelurahan::select('id', 'nama')->where('kecamatan_id', $filters['kecamatan_id'])->orderBy('nama')->get()
|
||||
: collect();
|
||||
$komoditasOptions = Komoditas::select('id', 'nama')->orderBy('nama')->get();
|
||||
$jenisKelompokOptions = KelompokTani::query()->whereNotNull('jenis_kelompok')
|
||||
->where('jenis_kelompok', '!=', '')->distinct()->orderBy('jenis_kelompok')->pluck('jenis_kelompok');
|
||||
|
||||
return view('produktivitas.survey-kelompok-tani.index', compact(
|
||||
'kelompokTani', 'summary', 'filters', 'selectedKabupaten',
|
||||
'kecamatanOptions', 'desaOptions', 'komoditasOptions', 'jenisKelompokOptions'
|
||||
));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||
$komoditas = Komoditas::where('is_utama', true)->get();
|
||||
|
||||
return view('produktivitas.survey-kelompok-tani.add', [
|
||||
...compact('provinsi', 'komoditas'),
|
||||
...$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)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$validated = $this->validateIdentitas($request);
|
||||
|
||||
if (($validated['jenis_kelompok'] ?? null) === 'Lainnya') {
|
||||
$validated['jenis_kelompok'] = $validated['jenis_kelompok_lainnya'];
|
||||
}
|
||||
unset($validated['jenis_kelompok_lainnya']);
|
||||
|
||||
$kelompokTani = KelompokTani::create([
|
||||
...$validated,
|
||||
'uid' => 'KLP-' . strtoupper(Str::random(10)),
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil ditambahkan.', 'pengurus');
|
||||
}
|
||||
|
||||
private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string
|
||||
{
|
||||
$url = route('kelompok-tani.edit', $kelompokTani->id);
|
||||
|
||||
return $section === 'identitas' ? $url : $url . '?section=' . $section;
|
||||
}
|
||||
|
||||
public function edit(Request $request, KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
return match ($request->query('section', 'identitas')) {
|
||||
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')),
|
||||
'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', [
|
||||
'kelompokTani' => $kelompokTani->loadMissing(['aset', 'bantuan']),
|
||||
]),
|
||||
'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani')),
|
||||
default => view('produktivitas.survey-kelompok-tani.edit', [
|
||||
'kelompokTani' => $kelompokTani->loadMissing('pengurus'),
|
||||
'provinsi' => Provinsi::orderBy('kode', 'asc')->get(),
|
||||
'komoditas' => Komoditas::where('is_utama', true)->get(),
|
||||
...$this->profileLocationOptions($kelompokTani),
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
public function update(Request $request, KelompokTani $kelompokTani)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
return match ($request->input('section', 'identitas')) {
|
||||
'ringkasan-pengurus' => $this->updateRingkasanPengurus($request, $kelompokTani),
|
||||
'kelembagaan' => $this->updateKelembagaan($request, $kelompokTani),
|
||||
'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani),
|
||||
'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani),
|
||||
'sosial-konflik' => $this->updateSosialKonflik($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),
|
||||
};
|
||||
}
|
||||
|
||||
public function destroy(string $id)
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$kelompokTani = KelompokTani::findOrFail($id);
|
||||
$kelompokTani->update(['deleted_by' => auth()->id()]);
|
||||
$kelompokTani->delete();
|
||||
|
||||
return response()->json(['message' => 'Data kelompok tani berhasil dihapus.']);
|
||||
}
|
||||
|
||||
private function validateIdentitas(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
// Data Responden
|
||||
'nama_responden' => ['required', 'string', 'max:255'],
|
||||
'jabatan_responden' => ['nullable', 'string', 'max:255'],
|
||||
'kontak_responden' => ['nullable', 'string', 'max:20'],
|
||||
|
||||
// Identitas Kelompok Tani
|
||||
'nama_kelompok' => ['required', 'string', 'max:255'],
|
||||
'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'],
|
||||
'jenis_kelompok_lainnya' => ['required_if:jenis_kelompok,Lainnya', 'nullable', 'string', 'max:255'],
|
||||
'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' => '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)
|
||||
{
|
||||
$validated = $this->validateIdentitas($request);
|
||||
|
||||
if (($validated['jenis_kelompok'] ?? null) === 'Lainnya') {
|
||||
$validated['jenis_kelompok'] = $validated['jenis_kelompok_lainnya'];
|
||||
}
|
||||
unset($validated['jenis_kelompok_lainnya']);
|
||||
|
||||
$kelompokTani->update([
|
||||
...$validated,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
|
||||
}
|
||||
|
||||
private function keanggotaanRules(): array
|
||||
{
|
||||
return [
|
||||
'jumlah_pengurus' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_anggota_aktif' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_pengurus_laki_laki' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_pengurus_perempuan' => ['nullable', 'integer', 'min:0'],
|
||||
'jumlah_anggota_laki_laki' => ['nullable', 'integer', 'min:0'],
|
||||
'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($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'],
|
||||
'nama_wilayah_cakupan' => ['nullable', 'string'],
|
||||
|
||||
// Status Legalitas Kelompok
|
||||
'status_legalitas' => ['nullable', 'string', 'max:50'],
|
||||
'nomor_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:100'],
|
||||
'tanggal_tahun_penerbitan_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:30'],
|
||||
|
||||
]);
|
||||
|
||||
$kelompokTani->update([
|
||||
...$validated,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data Kelembagaan berhasil disimpan.');
|
||||
}
|
||||
|
||||
private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array
|
||||
{
|
||||
if (empty($selected)) {
|
||||
return $selected;
|
||||
}
|
||||
|
||||
return array_values(array_map(function ($item) use ($lainnyaText) {
|
||||
return $item === 'Lainnya' && filled($lainnyaText) ? $lainnyaText : $item;
|
||||
}, $selected));
|
||||
}
|
||||
|
||||
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'],
|
||||
'kegiatan_terlaksana_lainnya' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
'estimasi_panen_minimal' => ['nullable', 'numeric', 'min:0'],
|
||||
'estimasi_panen_maksimal' => ['nullable', 'numeric', 'min:0', 'gte:estimasi_panen_minimal'],
|
||||
'satuan_hasil_panen' => ['nullable', 'string', 'max:20'],
|
||||
|
||||
'tindakan_hasil_panen' => ['nullable', 'array'],
|
||||
'tindakan_hasil_panen.*' => ['string', 'max:255'],
|
||||
|
||||
// Peran dan Fungsi Kelompok
|
||||
'fungsi_kelompok_bagi_anggota' => ['nullable', 'array'],
|
||||
'fungsi_kelompok_bagi_anggota.*' => ['string', 'max:255'],
|
||||
'fungsi_kelompok_lainnya' => ['nullable', 'string', 'max:255'],
|
||||
], [
|
||||
'estimasi_panen_maksimal.gte' => 'Estimasi panen maksimal tidak boleh kurang dari estimasi minimal.',
|
||||
]);
|
||||
|
||||
$validated['kegiatan_terlaksana'] = $this->resolveLainnya(
|
||||
$validated['kegiatan_terlaksana'] ?? null,
|
||||
$validated['kegiatan_terlaksana_lainnya'] ?? null
|
||||
);
|
||||
unset($validated['kegiatan_terlaksana_lainnya']);
|
||||
|
||||
$validated['fungsi_kelompok_bagi_anggota'] = $this->resolveLainnya(
|
||||
$validated['fungsi_kelompok_bagi_anggota'] ?? null,
|
||||
$validated['fungsi_kelompok_lainnya'] ?? null
|
||||
);
|
||||
unset($validated['fungsi_kelompok_lainnya']);
|
||||
|
||||
$kelompokTani->update([
|
||||
...$validated,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data Operasional berhasil disimpan.', 'aset-baku');
|
||||
}
|
||||
|
||||
private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'pembukuan_kas' => ['nullable', 'string', 'max:50'],
|
||||
'pengelola_kas' => ['nullable', 'string', 'max:255'],
|
||||
'masalah_pengelolaan_kas_simpan_pinjam' => ['nullable', 'string', 'max:255'],
|
||||
'deskripsi_masalah_pengelolaan_kas_simpan_pinjam' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$kelompokTani->update([
|
||||
...$validated,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
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' => [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 (!$pernahKonflik) {
|
||||
$validated['topik_konflik'] = null;
|
||||
$validated['penyelesaian_konflik'] = null;
|
||||
}
|
||||
|
||||
$kelompokTani->update([
|
||||
...$validated,
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data Sosial berhasil disimpan.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,26 +3,53 @@
|
||||
namespace App\Http\Controllers\Web\Produktivitas;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\SurveyProduktivitas;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SurveyProduktivitasController extends Controller
|
||||
{
|
||||
public function store($petaniId, Request $request)
|
||||
{
|
||||
$petani = Petani::findOrFail($petaniId);
|
||||
$validated = $request->validate([
|
||||
'produktivitas_komoditas' => ['required', 'array'],
|
||||
'produktivitas_komoditas.*.komoditas_id' => ['nullable', 'exists:master_komoditas,id'],
|
||||
'produktivitas_komoditas.*.hasil_panen_3_musim' => ['nullable', 'numeric'],
|
||||
'produktivitas_komoditas.*.satuan' => ['nullable', 'string'],
|
||||
|
||||
// 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'],
|
||||
'produktivitas_komoditas.*.hasil_panen_3_musim' => ['required', 'numeric', 'min:0'],
|
||||
'produktivitas_komoditas.*.satuan' => ['required', 'in:ha_tahun,pohon_tahun'],
|
||||
], [
|
||||
'produktivitas_komoditas.required' => 'Minimal satu data produktivitas harus diisi.',
|
||||
'produktivitas_komoditas.*.komoditas_id.required' => 'Komoditas wajib dipilih.',
|
||||
'produktivitas_komoditas.*.komoditas_id.exists' => 'Komoditas yang dipilih tidak valid.',
|
||||
'produktivitas_komoditas.*.hasil_panen_3_musim.required' => 'Hasil panen wajib diisi.',
|
||||
'produktivitas_komoditas.*.hasil_panen_3_musim.numeric' => 'Hasil panen harus berupa angka.',
|
||||
'produktivitas_komoditas.*.hasil_panen_3_musim.min' => 'Hasil panen tidak boleh kurang dari 0.',
|
||||
'produktivitas_komoditas.*.satuan.required' => 'Satuan wajib dipilih.',
|
||||
'produktivitas_komoditas.*.satuan.in' => 'Satuan yang dipilih tidak valid.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->redirectBackToFragment($request)->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$validated = $validator->validated();
|
||||
|
||||
foreach ($validated['produktivitas_komoditas'] as $row) {
|
||||
SurveyProduktivitas::create([
|
||||
'uid' => 'SPV-' . strtoupper(Str::random(10)),
|
||||
'uid' => 'SPV-'.strtoupper(Str::random(10)),
|
||||
'petani_id' => $petani->id,
|
||||
'komoditas_id' => $row['komoditas_id'],
|
||||
'kuantitas' => $row['hasil_panen_3_musim'],
|
||||
@@ -32,10 +59,10 @@ class SurveyProduktivitasController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Data produktivitas berhasil disimpan.');
|
||||
return $this->redirectBackToFragment($request)->with('success', 'Data produktivitas berhasil disimpan.');
|
||||
}
|
||||
|
||||
public function destroy($petaniId, $surveyProduktivitasId)
|
||||
public function destroy($petaniId, $surveyProduktivitasId, Request $request)
|
||||
{
|
||||
$surveyProduktivitas = SurveyProduktivitas::where('petani_id', $petaniId)
|
||||
->where('id', $surveyProduktivitasId)
|
||||
@@ -44,6 +71,17 @@ class SurveyProduktivitasController extends Controller
|
||||
$surveyProduktivitas->save();
|
||||
$surveyProduktivitas->delete();
|
||||
|
||||
return back()->with('success', 'Data produktivitas berhasil dihapus.');
|
||||
return $this->redirectBackToFragment($request)->with('success', 'Data produktivitas berhasil dihapus.');
|
||||
}
|
||||
|
||||
private function redirectBackToFragment(Request $request)
|
||||
{
|
||||
$fragment = $request->input('redirect_fragment');
|
||||
|
||||
if (! is_string($fragment) || ! preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
|
||||
return back();
|
||||
}
|
||||
|
||||
return redirect()->to(Str::before(url()->previous(), '#').'#'.$fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\Role;
|
||||
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.
|
||||
*/
|
||||
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');
|
||||
|
||||
if ($petani !== null && $user->isKoordinator()) {
|
||||
$petani = $petani instanceof Petani ? $petani : Petani::find($petani);
|
||||
|
||||
abort_unless(
|
||||
$petani && $user->bolehAksesPetani($petani),
|
||||
403,
|
||||
'Petani ini bukan bagian dari komoditas yang Anda koordinasikan.'
|
||||
);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Bumdes;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class KelembagaanAktorBumdes extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'bumdes_kelembagaan_aktor';
|
||||
|
||||
protected $fillable = [
|
||||
'pengurus_bumdes_id',
|
||||
'status_badan_hukum',
|
||||
'nomor_dasar_hukum',
|
||||
'tahun_dasar_hukum',
|
||||
'status_keaktifan',
|
||||
'alasan_tidak_aktif',
|
||||
'unit_usaha',
|
||||
'unit_usaha_jasa_lainnya',
|
||||
'status_unit_komoditas',
|
||||
'nama_unit_komoditas',
|
||||
'status_ad_art',
|
||||
'tahun_ad_art',
|
||||
'pengurus_inti',
|
||||
'memiliki_staf',
|
||||
'jumlah_staf',
|
||||
'unit_usaha_staf',
|
||||
'modal_awal',
|
||||
'tahun_modal_awal',
|
||||
'pernah_penambahan_modal',
|
||||
'penambahan_modal',
|
||||
'aset_produktif',
|
||||
'aset_produktif_lainnya',
|
||||
'status_kepemilikan_aset',
|
||||
'infrastruktur_pascapanen',
|
||||
'infrastruktur_pascapanen_lainnya',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tahun_dasar_hukum' => 'integer',
|
||||
'tahun_ad_art' => 'integer',
|
||||
'unit_usaha' => 'array',
|
||||
'pengurus_inti' => 'array',
|
||||
'memiliki_staf' => 'boolean',
|
||||
'jumlah_staf' => 'integer',
|
||||
'modal_awal' => 'decimal:2',
|
||||
'tahun_modal_awal' => 'integer',
|
||||
'pernah_penambahan_modal' => 'boolean',
|
||||
'penambahan_modal' => 'array',
|
||||
'aset_produktif' => 'array',
|
||||
'infrastruktur_pascapanen' => 'array',
|
||||
];
|
||||
|
||||
public function pengurusBumdes(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PengurusBumdes::class, 'pengurus_bumdes_id');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Bumdes;
|
||||
|
||||
use App\Enums\Bumdes\JabatanBumdes;
|
||||
use App\Enums\Pendidikan;
|
||||
use App\Models\User;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PengurusBumdes extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'bumdes_pengurus';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'nama',
|
||||
'jenis_kelamin',
|
||||
'tempat_lahir',
|
||||
'tanggal_lahir',
|
||||
'pendidikan_terakhir',
|
||||
'alamat',
|
||||
'desa_kelurahan_id',
|
||||
'nomor_telepon',
|
||||
'jabatan',
|
||||
'jabatan_lainnya',
|
||||
'lama_menjabat_tahun',
|
||||
'tanggal_mulai_menjabat',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'jenis_kelamin' => 'boolean',
|
||||
'tanggal_lahir' => 'date',
|
||||
'pendidikan_terakhir' => Pendidikan::class,
|
||||
'tanggal_mulai_menjabat' => 'date',
|
||||
'jabatan' => JabatanBumdes::class,
|
||||
];
|
||||
|
||||
public function getUsiaAttribute(): ?int
|
||||
{
|
||||
if (! $this->tanggal_lahir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::parse($this->tanggal_lahir)->age;
|
||||
}
|
||||
|
||||
public function getLamaMenjabatAttribute(): string
|
||||
{
|
||||
if (! is_null($this->lama_menjabat_tahun)) {
|
||||
return $this->lama_menjabat_tahun . ' tahun';
|
||||
}
|
||||
|
||||
if (! $this->tanggal_mulai_menjabat) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$tanggalMulai = Carbon::parse($this->tanggal_mulai_menjabat)->startOfDay();
|
||||
|
||||
if ($tanggalMulai->isFuture()) {
|
||||
return 'Belum mulai';
|
||||
}
|
||||
|
||||
$selisih = $tanggalMulai->diff(Carbon::now());
|
||||
$bagian = [];
|
||||
|
||||
if ($selisih->y > 0) {
|
||||
$bagian[] = $selisih->y . ' tahun';
|
||||
}
|
||||
|
||||
if ($selisih->m > 0) {
|
||||
$bagian[] = $selisih->m . ' bulan';
|
||||
}
|
||||
|
||||
if (empty($bagian)) {
|
||||
return $selisih->d . ' hari';
|
||||
}
|
||||
|
||||
return implode(' ', $bagian);
|
||||
}
|
||||
|
||||
public function getNamaJabatanAttribute(): string
|
||||
{
|
||||
if ($this->jabatan === JabatanBumdes::LAINNYA && $this->jabatan_lainnya) {
|
||||
return $this->jabatan_lainnya;
|
||||
}
|
||||
|
||||
return $this->jabatan?->label() ?? '-';
|
||||
}
|
||||
|
||||
public function desaKelurahan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DesaKelurahan::class, 'desa_kelurahan_id');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function kelembagaanAktor(): HasOne
|
||||
{
|
||||
return $this->hasOne(KelembagaanAktorBumdes::class, 'pengurus_bumdes_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
// Mengisi kolom deleted_by otomatis saat data di-soft delete (AISTANDAR 5.3).
|
||||
trait HasDeletedBy
|
||||
{
|
||||
protected static function bootHasDeletedBy(): void
|
||||
{
|
||||
static::deleting(function ($model) {
|
||||
if (! $model->isForceDeleting()) {
|
||||
$model->deleted_by = auth()->id();
|
||||
$model->saveQuietly();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Enums\StatusDataPetani;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Data petani yang sudah ditandai "Lengkap" lalu diubah/ditambah/dihapus
|
||||
* otomatis kembali ke "Draf", supaya enumerator menandai lengkap lagi setelah selesai.
|
||||
*
|
||||
* Dipasang di semua model yang menyimpan data profil petani (web maupun API).
|
||||
* Model yang tidak punya kolom petani_id meng-override petaniIdUntukStatus().
|
||||
*/
|
||||
trait KembalikanStatusPetaniKeDraf
|
||||
{
|
||||
// Kolom otomatis/audit yang tidak dihitung sebagai perubahan data.
|
||||
private static array $kolomBukanPerubahanData = [
|
||||
'created_at', 'updated_at', 'deleted_at', 'created_by', 'updated_by', 'deleted_by', 'status',
|
||||
];
|
||||
|
||||
public static function bootKembalikanStatusPetaniKeDraf(): void
|
||||
{
|
||||
static::saved(function ($model) {
|
||||
$kolomBerubah = array_diff(array_keys($model->getChanges()), self::$kolomBukanPerubahanData);
|
||||
|
||||
if ($model->wasRecentlyCreated || $kolomBerubah) {
|
||||
$model->kembalikanStatusPetaniKeDraf();
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(fn ($model) => $model->kembalikanStatusPetaniKeDraf());
|
||||
}
|
||||
|
||||
protected function petaniIdUntukStatus(): ?int
|
||||
{
|
||||
return $this->petani_id;
|
||||
}
|
||||
|
||||
public function kembalikanStatusPetaniKeDraf(): void
|
||||
{
|
||||
$petaniId = $this->petaniIdUntukStatus();
|
||||
if (! $petaniId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Satu simpan bisa memicu ratusan baris (mis. sinkron jenis hama ke semua tanaman).
|
||||
// Cukup sekali per petani per request: setelah dicek, status pasti bukan "Lengkap" lagi.
|
||||
self::ingatPerRequest('status_petani.dicek', $petaniId, function () use ($petaniId) {
|
||||
// Query langsung (tanpa event model) supaya tidak memicu hook lain / berulang.
|
||||
$diubah = Petani::withoutGlobalScopes()
|
||||
->whereKey($petaniId)
|
||||
->where('status', StatusDataPetani::LENGKAP->value)
|
||||
->update(['status' => StatusDataPetani::DRAFT->value]);
|
||||
|
||||
if ($diubah && request()->hasSession()) {
|
||||
request()->session()->flash('status_petani_info', 'Data petani diubah, status kembali ke Draf. Tandai lengkap lagi setelah selesai.');
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan hasil $ambil() selama satu request web, per $kunci + $id.
|
||||
* Di console/queue tidak disimpan (proses berjalan lama, data bisa berubah antar-job).
|
||||
*/
|
||||
protected static function ingatPerRequest(string $kunci, int|string|null $id, Closure $ambil): mixed
|
||||
{
|
||||
if ($id === null || app()->runningInConsole()) {
|
||||
return $ambil();
|
||||
}
|
||||
|
||||
$bag = request()->attributes;
|
||||
$cache = $bag->get($kunci, []);
|
||||
|
||||
if (! array_key_exists($id, $cache)) {
|
||||
$cache[$id] = $ambil();
|
||||
$bag->set($kunci, $cache);
|
||||
}
|
||||
|
||||
return $cache[$id];
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ class FormBasisData extends Model
|
||||
public function form(): HasMany
|
||||
{
|
||||
return $this->hasMany(FormBasisData::class, 'parent_id')
|
||||
->with('form')
|
||||
->orderBy('urutan');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Konfigurasi;
|
||||
|
||||
use App\Models\Concerns\HasDeletedBy;
|
||||
use App\Models\Pertanian\Komoditas;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class KoordinatorKomoditas extends Model
|
||||
{
|
||||
use SoftDeletes, HasDeletedBy;
|
||||
|
||||
protected $table = 'koordinator_komoditas';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'komoditas_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function komoditas(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Komoditas::class, 'komoditas_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Koperasi;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PengurusKoperasi extends Model
|
||||
{
|
||||
protected $table = 'pengurus_koperasi';
|
||||
|
||||
protected $fillable = [
|
||||
// Profil Dassar
|
||||
'nama_lengkap', 'posisi_dalam_koperasi', 'tahun_bergabung',
|
||||
'no_hp', 'alamat_tinggal',
|
||||
|
||||
// Profil Koperasi
|
||||
'nama_koperasi', 'jenis_koperasi', 'alamat_koperasi',
|
||||
'skala_operasi', 'tahun_berdiri',
|
||||
|
||||
// Anggota & Pengurus
|
||||
'jumlah_anggota_awal', 'jumlah_anggota_dewasa', 'jumlah_anggota_anak', 'jumlah_pengurus',
|
||||
|
||||
// SHU
|
||||
'shu_2025', 'shu_2024', 'shu_2023',
|
||||
|
||||
// Kelembagaan & Manajemen
|
||||
'sejarah_lembaga', 'fokus_komoditas', 'detail_fokus_komoditas',
|
||||
'ad_art', 'tahun_ad_art', 'layanan_koperasi', 'layanan_lainnya',
|
||||
'pelatihan_produksi', 'pelatihan_pascapanen', 'pelatihan_pengolahan',
|
||||
'pelatihan_keuangan', 'pelatihan_pemasaran', 'pelatihan_lainnya',
|
||||
'status_pembukuan', 'format_pembukuan', 'pertemuan_rutin', 'pertemuan_lainnya',
|
||||
'kasus_kredit_macet', 'sanksi_kredit_macet', 'efektivitas_sanksi',
|
||||
'faktor_manajemen', 'faktor_manajemen_lainnya', 'keterlibatan_rantai_nilai',
|
||||
'agregasi_komoditas', 'agregasi_komoditas_lainnya', 'fasilitas_koperasi', 'fasilitas_lainnya',
|
||||
'pengolahan_lanjutan', 'detail_pengolahan_lanjutan', 'cara_penentuan_harga',
|
||||
'kontrak_offtaker', 'kontrak_komoditas',
|
||||
|
||||
// Akses Pasar & Rantai Nilai Eksternal
|
||||
'sumber_pasokan', 'sumber_pasokan_lainnya', 'bentuk_pembelian',
|
||||
'volume_beli_sekarang', 'volume_beli_1_tahun', 'volume_beli_2_tahun',
|
||||
'tujuan_penjualan', 'tujuan_penjualan_lainnya', 'bentuk_penjualan',
|
||||
'volume_jual_sekarang', 'volume_jual_1_tahun', 'volume_jual_2_tahun',
|
||||
'sifat_hubungan_bisnis', 'sifat_hubungan_lainnya', 'mekanisme_pembayaran', 'status_perjanjian_tertulis',
|
||||
|
||||
// Harga Komoditas
|
||||
'harga_segar_sekarang', 'harga_segar_1_tahun', 'harga_segar_2_tahun',
|
||||
'harga_basah_sekarang', 'harga_basah_1_tahun', 'harga_basah_2_tahun',
|
||||
'harga_kering_sekarang', 'harga_kering_1_tahun', 'harga_kering_2_tahun',
|
||||
'harga_fermentasi_sekarang', 'harga_fermentasi_1_tahun', 'harga_fermentasi_2_tahun',
|
||||
'cara_menetapkan_harga_pasar', 'cara_menetapkan_harga_lainnya',
|
||||
|
||||
// Praktik Kualitas & Pascapanen
|
||||
'praktik_sortasi', 'standar_sortasi', 'praktik_pengeringan', 'cara_pengeringan',
|
||||
'praktik_fermentasi', 'model_fermentasi', 'proses_fermentasi',
|
||||
'harga_premium_rupiah', 'harga_premium_persen',
|
||||
|
||||
// Sosial Ekonomi
|
||||
'simpanan_terbesar', 'momen_simpanan_terbesar',
|
||||
'alasan_pinjaman', 'alasan_pinjaman_lainnya', 'pembayaran_selain_uang',
|
||||
'bentuk_pembayaran_barang', 'bentuk_pembayaran_lainnya', 'perhitungan_harga_pasar', 'alasan_bukan_harga_pasar',
|
||||
|
||||
// PENILAIAN ENUMERATOR
|
||||
'enum_potensi', 'enum_alasan_potensi', 'enum_kebutuhan', 'enum_kebutuhan_lain', 'enum_keterbukaan'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Master;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class JenisTanaman extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'master_jenis_tanaman';
|
||||
|
||||
protected $fillable = [
|
||||
'nama',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -15,22 +16,32 @@ use App\Enums\AksesPasar\SkorKesadaranPremium;
|
||||
|
||||
class AksesPasar extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'akses_pasar';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'petani_id',
|
||||
'tujuan_penjualan',
|
||||
'tujuan_penjualan_lainnya',
|
||||
'sifat_hubungan_bisnis',
|
||||
'bentuk_penjualan',
|
||||
'frekuensi_penjualan',
|
||||
'skor_pengalaman',
|
||||
'saluran_terdahulu',
|
||||
'saluran_terdahulu_lainnya',
|
||||
'alasan_berhenti',
|
||||
'tahu_harga_sebelumnya',
|
||||
'sumber_info_harga',
|
||||
'sumber_info_harga_lainnya',
|
||||
'praktik_fermentasi',
|
||||
'proses_fermentasi',
|
||||
'model_fermentasi_memungkinkan',
|
||||
'skor_kesadaran_premium',
|
||||
'sumber_info_premium',
|
||||
'tingkat_kepercayaan_pembeli_baru',
|
||||
'alasan_kepercayaan_pembeli_baru',
|
||||
'waktu_mulai_wawancara',
|
||||
'waktu_selesai_wawancara',
|
||||
'skor_kepercayaan_responden',
|
||||
@@ -42,7 +53,9 @@ class AksesPasar extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tujuan_penjualan' => 'array',
|
||||
'saluran_terdahulu' => 'array',
|
||||
'sumber_info_harga' => 'array',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
@@ -50,7 +63,7 @@ class AksesPasar extends Model
|
||||
static::saved(function (AksesPasar $aksesPasar) {
|
||||
|
||||
$petani = Petani::find($aksesPasar->petani_id);
|
||||
$skor = SkorPetani::where('petani_id')->orderBy('id')->first();
|
||||
$skor = SkorPetani::where('petani_id', $petani->id)->orderBy('id')->first();
|
||||
if (!$skor) {
|
||||
$petaniUid = $petani->uid;
|
||||
$part = explode('-', $petaniUid);
|
||||
@@ -61,10 +74,11 @@ class AksesPasar extends Model
|
||||
'updated_by' => $petani->updated_by
|
||||
]);
|
||||
}
|
||||
|
||||
$skor->update([
|
||||
'skor_d6_k1_6_pengalaman_menjual_ke_saluran_berbeda' => $aksesPasar->skor_pengalaman,
|
||||
'skor_d6_k4_1_kesadaran_akses_pasar_premium' => $aksesPasar->skor_kesadaran_premium,
|
||||
'skor_d6_k5_1_hambatan_mutu' => $aksesPasar->skor_kepercayaan_responden
|
||||
// skor_d6_k5_1_hambatan_mutu diisi dari SurveyPanen (tingkat hasil panen ditolak).
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AsetKelompokTani extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'survey_aset_kelompok_tani';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'survey_kelompok_tani_id',
|
||||
'jenis_aset',
|
||||
'status_aset',
|
||||
'status_pengelolaan_aset',
|
||||
'kondisi_aset',
|
||||
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status_aset' => 'boolean',
|
||||
];
|
||||
|
||||
public function kelompokTani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BantuanKelompokTani extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'survey_bantuan_kelompok_tani';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'survey_kelompok_tani_id',
|
||||
'nama_program',
|
||||
'penyelenggara',
|
||||
'bentuk_bantuan',
|
||||
'tahun',
|
||||
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
public function kelompokTani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\User;
|
||||
use App\Models\Pertanian\Petani;
|
||||
|
||||
class HargaJualPanen extends Model
|
||||
{
|
||||
use KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'harga_jual_panen';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
|
||||
class KelompokTani extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const STATUS_LEGALITAS_DENGAN_SK = [
|
||||
'Sudah ada SK dari desa/kecamatan',
|
||||
'Sudah ada SK dan terdaftar di Dinas/Kabupaten',
|
||||
];
|
||||
|
||||
protected $table = 'survey_kelompok_tani';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
|
||||
// Data Responden
|
||||
'nama_responden',
|
||||
'jabatan_responden',
|
||||
'kontak_responden',
|
||||
|
||||
// Identitas Kelompok Tani
|
||||
'nama_kelompok',
|
||||
'desa_kelurahan_id',
|
||||
'dusun',
|
||||
'tahun_berdiri',
|
||||
'jenis_kelompok',
|
||||
'nama_gapoktan',
|
||||
'komoditas_id',
|
||||
|
||||
// Pengurus & Anggota Kelompok
|
||||
'jumlah_pengurus',
|
||||
'jumlah_pengurus_aktif',
|
||||
'jumlah_anggota_terdaftar',
|
||||
'jumlah_anggota_aktif',
|
||||
'jumlah_pengurus_laki_laki',
|
||||
'jumlah_pengurus_perempuan',
|
||||
'jumlah_anggota_laki_laki',
|
||||
'jumlah_anggota_perempuan',
|
||||
'jumlah_anggota_pemuda_laki_laki',
|
||||
'jumlah_anggota_pemuda_perempuan',
|
||||
|
||||
// Cakupan Wilayah Kelompok
|
||||
'cakupan_wilayah',
|
||||
'nama_wilayah_cakupan',
|
||||
|
||||
// Status Legalitas Kelompok
|
||||
'status_legalitas',
|
||||
'nomor_sk',
|
||||
'tanggal_tahun_penerbitan_sk',
|
||||
|
||||
// Kualitas Kepemimpinan
|
||||
'tingkat_keaktifan_ketua',
|
||||
'status_pembagian_peran',
|
||||
'status_dokumentasi_rapat',
|
||||
|
||||
// Frekuensi Pertemuan Rutin
|
||||
'frekuensi_pertemuan',
|
||||
'tanggal_pertemuan_terakhir',
|
||||
|
||||
// Kegiatan Konkret yang Sudah Berjalan
|
||||
'kegiatan_terlaksana',
|
||||
'estimasi_panen_minimal',
|
||||
'estimasi_panen_maksimal',
|
||||
'satuan_hasil_panen',
|
||||
'tindakan_hasil_panen',
|
||||
|
||||
// Peran & Fungsi Kelompok
|
||||
'fungsi_kelompok_bagi_anggota',
|
||||
|
||||
// Kapasitas Pengelolaan Kas/Simpan Pinjam
|
||||
'pembukuan_kas',
|
||||
'pengelola_kas',
|
||||
'masalah_pengelolaan_kas_simpan_pinjam',
|
||||
'deskripsi_masalah_pengelolaan_kas_simpan_pinjam',
|
||||
|
||||
// Keterlibatan Perempuan
|
||||
'partisipasi_perempuan',
|
||||
|
||||
// Konflik Internal
|
||||
'konflik_kelompok',
|
||||
'topik_konflik',
|
||||
'penyelesaian_konflik',
|
||||
|
||||
// Pengalaman Bantuan
|
||||
'pernah_mendapatkan_bantuan',
|
||||
|
||||
// Postensi Sebagai Mitra Agregasi/Pengolahan Kolektif
|
||||
'potensi_kelompok',
|
||||
'alasan_penilaian_potensi',
|
||||
|
||||
// Audit
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'komoditas_id' => 'array',
|
||||
'tindakan_hasil_panen' => 'array',
|
||||
'fungsi_kelompok_bagi_anggota' => 'array',
|
||||
'kegiatan_terlaksana' => 'array',
|
||||
|
||||
'pernah_mendapatkan_bantuan' => 'boolean',
|
||||
|
||||
'tanggal_pertemuan_terakhir' => 'date',
|
||||
|
||||
'estimasi_panen_minimal' => 'decimal:2',
|
||||
'estimasi_panen_maksimal' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function desaKelurahan()
|
||||
{
|
||||
return $this->belongsTo(DesaKelurahan::class, 'desa_kelurahan_id');
|
||||
}
|
||||
|
||||
public function pengurus()
|
||||
{
|
||||
return $this->hasMany(PengurusKelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
|
||||
public function aset()
|
||||
{
|
||||
return $this->hasMany(AsetKelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
|
||||
public function bantuan()
|
||||
{
|
||||
return $this->hasMany(BantuanKelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,17 @@ class Komoditas extends Model
|
||||
|
||||
protected $table = 'master_komoditas';
|
||||
|
||||
protected $casts = [
|
||||
'is_utama' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'nama',
|
||||
'kode',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
'is_utama',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
@@ -18,10 +19,11 @@ use App\Enums\KondisiTanaman;
|
||||
use App\Enums\SeranganHama;
|
||||
use App\Enums\PenyakitTanaman;
|
||||
use App\Enums\KeparahanSerangan;
|
||||
use App\Models\Pertanian\Petani;
|
||||
|
||||
class Lahan extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'map_lahan';
|
||||
|
||||
@@ -56,7 +58,10 @@ class Lahan extends Model
|
||||
'komoditas_lain',
|
||||
'tanaman_penaung',
|
||||
|
||||
'foto'
|
||||
'foto',
|
||||
|
||||
'jenis_hama',
|
||||
'jenis_penyakit',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -68,7 +73,9 @@ class Lahan extends Model
|
||||
'serangan_hama' => SeranganHama::class,
|
||||
'penyakit_tanaman' => PenyakitTanaman::class,
|
||||
'keparahan_serangan' => KeparahanSerangan::class,
|
||||
'foto' => 'array'
|
||||
'foto' => 'array',
|
||||
'jenis_hama' => 'array',
|
||||
'jenis_penyakit' => 'array'
|
||||
];
|
||||
|
||||
public function profile(): BelongsTo
|
||||
@@ -93,7 +100,7 @@ class Lahan extends Model
|
||||
|
||||
public function petani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'petani_id');
|
||||
return $this->belongsTo(Petani::class, 'petani_id');
|
||||
}
|
||||
|
||||
public function komoditas(): BelongsTo
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PendapatanKomoditas extends Model
|
||||
{
|
||||
use KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'pendapatan_komoditas';
|
||||
|
||||
protected $fillable = [
|
||||
@@ -29,4 +32,11 @@ class PendapatanKomoditas extends Model
|
||||
{
|
||||
return $this->belongsTo(SosialEkonomi::class, 'sosial_ekonomi_id');
|
||||
}
|
||||
|
||||
// Untuk trait KembalikanStatusPetaniKeDraf: petani diambil dari data sosial ekonomi.
|
||||
protected function petaniIdUntukStatus(): ?int
|
||||
{
|
||||
return self::ingatPerRequest('status_petani.sosial_ekonomi', $this->sosial_ekonomi_id,
|
||||
fn () => SosialEkonomi::withoutGlobalScopes()->whereKey($this->sosial_ekonomi_id)->value('petani_id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PendapatanLuarPertanian extends Model
|
||||
{
|
||||
use KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'pendapatan_luar_pertanian';
|
||||
|
||||
protected $fillable = [
|
||||
@@ -27,4 +30,11 @@ class PendapatanLuarPertanian extends Model
|
||||
{
|
||||
return $this->belongsTo(SosialEkonomi::class, 'sosial_ekonomi_id');
|
||||
}
|
||||
|
||||
// Untuk trait KembalikanStatusPetaniKeDraf: petani diambil dari data sosial ekonomi.
|
||||
protected function petaniIdUntukStatus(): ?int
|
||||
{
|
||||
return self::ingatPerRequest('status_petani.sosial_ekonomi', $this->sosial_ekonomi_id,
|
||||
fn () => SosialEkonomi::withoutGlobalScopes()->whereKey($this->sosial_ekonomi_id)->value('petani_id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PengurusKelompokTani extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'survey_pengurus_kelompok_tani';
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'survey_kelompok_tani_id',
|
||||
'nama_pengurus',
|
||||
'jabatan',
|
||||
'kontak',
|
||||
'status',
|
||||
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
|
||||
public function kelompokTani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KelompokTani::class, 'survey_kelompok_tani_id');
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use App\Enums\Pendidikan;
|
||||
use App\Enums\Pernikahan;
|
||||
use App\Enums\Role;
|
||||
use App\Enums\StatusDataPetani;
|
||||
use App\Models\User;
|
||||
use App\Models\Wilayah\DesaKelurahan;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
@@ -28,7 +31,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
class Petani extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'petani';
|
||||
|
||||
@@ -70,6 +73,12 @@ class Petani extends Model
|
||||
|
||||
'pendidikan_terakhir' => Pendidikan::class,
|
||||
'status_perkawinan' => Pernikahan::class,
|
||||
'status' => StatusDataPetani::class,
|
||||
];
|
||||
|
||||
// Sengaja tidak masuk $fillable: status hanya diubah lewat PetaniStatusController.
|
||||
protected $attributes = [
|
||||
'status' => 'draft',
|
||||
];
|
||||
|
||||
public function getUmurAttribute(): ?int
|
||||
@@ -94,15 +103,15 @@ class Petani extends Model
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(function (Petani $petani) {
|
||||
|
||||
$skor = SkorPetani::where('petani_id')->orderBy('id')->first();
|
||||
if (! $skor) {
|
||||
$skor = SkorPetani::where('petani_id', $petani->id)->orderBy('id')->first();
|
||||
if (!$skor) {
|
||||
$petaniUid = $petani->uid;
|
||||
$part = explode('-', $petaniUid);
|
||||
$skor = SkorPetani::create([
|
||||
'uid' => 'SKR-'.$part[1].'-'.strtoupper(Str::random(10)),
|
||||
'petani_id' => $petani->id,
|
||||
'uid' => 'SKR-' . $part[1] . '-' . strtoupper(Str::random(10)),
|
||||
'created_by' => $petani->created_by,
|
||||
'updated_by' => $petani->updated_by,
|
||||
'updated_by' => $petani->updated_by
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -113,6 +122,20 @@ class Petani extends Model
|
||||
return $this->belongsTo(Komoditas::class, 'komoditas_id');
|
||||
}
|
||||
|
||||
// Hanya enumerator yang menginput petani ini yang boleh menandai Lengkap / membatalkannya.
|
||||
public function bisaDitandaiOleh(?User $user): bool
|
||||
{
|
||||
return $user
|
||||
&& $user->role === Role::ENUMERATOR
|
||||
&& (int) $this->created_by === (int) $user->id;
|
||||
}
|
||||
|
||||
// Pertanyaan penyerbukan manual hanya berlaku untuk petani vanili.
|
||||
public function isKomoditasVanili(): bool
|
||||
{
|
||||
return str_contains(strtolower($this->komoditas?->nama ?? ''), 'vanil');
|
||||
}
|
||||
|
||||
public function skor(): HasOne
|
||||
{
|
||||
return $this->hasOne(SkorPetani::class, 'petani_id');
|
||||
@@ -138,9 +161,9 @@ class Petani extends Model
|
||||
return $this->hasMany(SurveySertifikasi::class, 'petani_id');
|
||||
}
|
||||
|
||||
public function surveyProduktivitas(): HasOne
|
||||
public function surveyProduktivitas(): HasMany
|
||||
{
|
||||
return $this->hasOne(SurveyProduktivitas::class, 'petani_id')->latestOfMany();
|
||||
return $this->hasMany(SurveyProduktivitas::class, 'petani_id');
|
||||
}
|
||||
|
||||
public function lahans(): HasMany
|
||||
@@ -339,7 +362,7 @@ class Petani extends Model
|
||||
$skor >= 3.0 => 'Perintis',
|
||||
$skor >= 2.0 => 'Pemerhati',
|
||||
$skor >= 1.0 => 'Pemula',
|
||||
default => 'Belum tersegmentasi',
|
||||
default => 'Belum lengkap',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -365,10 +388,10 @@ class Petani extends Model
|
||||
} elseif ($skor->skor_kesiapan_adopsi >= 1.0) {
|
||||
return 'Pemula';
|
||||
} else {
|
||||
return 'Belum tersegmentasi';
|
||||
return 'Belum lengkap';
|
||||
}
|
||||
|
||||
return 'Belum tersegmentasi';
|
||||
return 'Belum lengkap';
|
||||
}
|
||||
|
||||
public function keteranganSegmen(): ?string
|
||||
@@ -398,4 +421,10 @@ class Petani extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// Untuk trait KembalikanStatusPetaniKeDraf: petani ini sendiri.
|
||||
protected function petaniIdUntukStatus(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
use App\Models\Pertanian\Tanaman;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
|
||||
class PraktikBudidaya extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'praktik_budidaya';
|
||||
|
||||
@@ -91,22 +95,75 @@ class PraktikBudidaya extends Model
|
||||
'updated_by' => $petani->updated_by
|
||||
]);
|
||||
}
|
||||
|
||||
$kebiasaan = $praktikBudidaya->kebiasaan_konservasi_tanah ? count($praktikBudidaya->kebiasaan_konservasi_tanah) : 0;
|
||||
$pemahaman = $praktikBudidaya->pemahaman_manfaat_konservasi_tanah == 'Ya, tahu manfaatnya' ? 1 : 0;
|
||||
$skor_konservasi_tanah = 0;
|
||||
$jenisHama = $praktikBudidaya->jenis_hama ?? [];
|
||||
$jenisPenyakit = $praktikBudidaya->jenis_penyakit ?? [];
|
||||
|
||||
// Salin jenis hama/penyakit ke semua lahan & tanaman petani: satu query per tabel
|
||||
// (sebelumnya satu query per baris; petani bisa punya ratusan titik tanaman).
|
||||
// Hanya baris yang isinya berbeda yang diubah, supaya updated_at baris lain tidak ikut berubah.
|
||||
self::sinkronHamaPenyakit(new Lahan, $praktikBudidaya, $jenisHama, $jenisPenyakit);
|
||||
self::sinkronHamaPenyakit(new Tanaman, $praktikBudidaya, $jenisHama, $jenisPenyakit);
|
||||
|
||||
if ($kebiasaan == 0) {
|
||||
$skor_konservasi_tanah = 1;
|
||||
} elseif ($kebiasaan == 1 && $pemahaman == 0) {
|
||||
$skor_konservasi_tanah = 2;
|
||||
} elseif ($kebiasaan == 1 && $pemahaman == 1) {
|
||||
$skor_konservasi_tanah = 3;
|
||||
} elseif ($kebiasaan > 1 && $pemahaman == 0) {
|
||||
$skor_konservasi_tanah = 4;
|
||||
} elseif ($kebiasaan > 1 && $pemahaman == 1) {
|
||||
$skor_konservasi_tanah = 5;
|
||||
}
|
||||
$skor->update([
|
||||
|
||||
'skor_d2_k2_3_praktik_pemangkasan' => $praktikBudidaya->frekuensi_pemangkasan_komoditas_utama,
|
||||
'skor_d2_k2_3_praktik_pemangkasan' => $praktikBudidaya->frekuensi_pemangkasan_utama,
|
||||
'skor_d2_k2_4_praktik_pemupukan' => $praktikBudidaya->pemberian_pupuk,
|
||||
'skor_d2_k2_5_pengendalian_opt' => $praktikBudidaya->metode_penanganan_hama_penyakit,
|
||||
'skor_d2_k2_5_pengendalian_opt' => $praktikBudidaya->metode_penanganan_opt,
|
||||
'skor_d2_k2_6_sanitasi_kebun' => $praktikBudidaya->frekuensi_sanitasi_lahan,
|
||||
'skor_d2_k2_6b_pengelolaan_gulma' => $praktikBudidaya->penanganan_gulma,
|
||||
'skor_d2_k2_7_penglolaan_air_irigasi' => $praktikBudidaya->kondisi_pengelolaan_air_irigasi,
|
||||
//'skor_d2_k2_8_praktik_konservasi_tanah' => $praktikBudidaya->kebiasaan_konservasi_tanah,
|
||||
//'skor_d2_k3_1_penentuan_pematangan' => $praktikBudidaya->
|
||||
'skor_d2_k2_7_penglolaan_air_irigasi' => $praktikBudidaya->kondisi_pengelolaan_air,
|
||||
'skor_d2_k2_8_praktik_konservasi_tanah' => $skor_konservasi_tanah,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/** @var array<string, bool> hasil cek kolom per tabel (cukup sekali per proses) */
|
||||
private static array $punyaKolomHama = [];
|
||||
|
||||
private static function sinkronHamaPenyakit(Model $model, PraktikBudidaya $praktikBudidaya, array $jenisHama, array $jenisPenyakit): void
|
||||
{
|
||||
$tabel = $model->getTable();
|
||||
$ada = self::$punyaKolomHama[$tabel] ??= Schema::hasColumns($tabel, ['jenis_hama', 'jenis_penyakit']);
|
||||
if (! $ada) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format JSON sama dengan cast 'array' di model Lahan/Tanaman.
|
||||
$hama = json_encode($jenisHama);
|
||||
$penyakit = json_encode($jenisPenyakit);
|
||||
|
||||
// Query builder: tidak memicu event per baris. Status petani sudah dikembalikan ke Draf
|
||||
// oleh event PraktikBudidaya sendiri (trait KembalikanStatusPetaniKeDraf).
|
||||
$model->newQuery()
|
||||
->where('petani_id', $praktikBudidaya->petani_id)
|
||||
// Kolom jsonb: dibandingkan sebagai jsonb (bukan teks) karena PostgreSQL menormalkan formatnya.
|
||||
->where(fn ($q) => $q
|
||||
->whereRaw('jenis_hama::jsonb IS DISTINCT FROM ?::jsonb', [$hama])
|
||||
->orWhereRaw('jenis_penyakit::jsonb IS DISTINCT FROM ?::jsonb', [$penyakit]))
|
||||
->update([
|
||||
'jenis_hama' => $hama,
|
||||
'jenis_penyakit' => $penyakit,
|
||||
'updated_by' => $praktikBudidaya->updated_by,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function petani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Petani::class, 'petani_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace App\Models\Pertanian;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
|
||||
class SkorPetani extends Model
|
||||
{
|
||||
@@ -50,13 +53,14 @@ class SkorPetani extends Model
|
||||
'skor_d4_k5_2_pengalaman_kredit_formal',
|
||||
'skor_d4_k5_3_kepemilikan_aset_produktif',
|
||||
'skor_d4_k5_4_kepemilikan_aset_tidak_produktif',
|
||||
'skor_d4_k5_4_5_akses_program_bantuan',
|
||||
'skor_d4_k5_4_akses_program_bantuan',
|
||||
'skor_d4_k6_3_keberadaan_hutang_terkait_panen',
|
||||
'skor_d4_k6_5a_praktik_ijon',
|
||||
'skor_d4_k6_5b_kebebasan_memilih_pembeli',
|
||||
'skor_d6_k1_6_pengalaman_menjual_ke_saluran_berbeda',
|
||||
'skor_d6_k4_1_kesadaran_akses_pasar_premium',
|
||||
'skor_d6_k5_1_hambatan_mutu',
|
||||
'skor_d6_k5_2_tingkat_kepercayaan_pembeli',
|
||||
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -102,7 +106,7 @@ class SkorPetani extends Model
|
||||
$skorPetani->skor_d4_k5_1_kepemilikan_rekening +
|
||||
$skorPetani->skor_d4_k5_2_pengalaman_kredit_formal +
|
||||
$skorPetani->skor_d4_k5_3_kepemilikan_aset_produktif +
|
||||
$skorPetani->skor_d4_k5_4_5_akses_program_bantuan
|
||||
$skorPetani->skor_d4_k5_4_akses_program_bantuan
|
||||
) / 4 +
|
||||
(
|
||||
$skorPetani->skor_d4_k6_3_keberadaan_hutang_terkait_panen +
|
||||
@@ -113,30 +117,36 @@ class SkorPetani extends Model
|
||||
$skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar = (
|
||||
$skorPetani->skor_d6_k1_6_pengalaman_menjual_ke_saluran_berbeda +
|
||||
$skorPetani->skor_d6_k4_1_kesadaran_akses_pasar_premium +
|
||||
$skorPetani->skor_d6_k5_1_hambatan_mutu
|
||||
) / 3;
|
||||
$skorPetani->skor_d6_k5_1_hambatan_mutu +
|
||||
$skorPetani->skor_d6_k5_2_tingkat_kepercayaan_pembeli
|
||||
) / 4;
|
||||
|
||||
$skorPetani->skor_d2_sistem_praktik_budidaya = (
|
||||
$skorPetani->skor_komp_3_d2_praktik_budidaya_gap
|
||||
);
|
||||
$skorPetani->skor_d4_sosial_ekonomi = (
|
||||
$skorPetani->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi
|
||||
);
|
||||
$skorPetani->skor_d6_rantai_nilai_akses_pasar = (
|
||||
$skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar * 0.25 +
|
||||
$skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar
|
||||
);
|
||||
$skorPetani->skor_d4_sosial_ekonomi = (
|
||||
$skorPetani->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi * 0.25 +
|
||||
$skorPetani->skor_komp_2_d4_k2_ekonomi_rumah_tangga * 0.20 +
|
||||
$skorPetani->skor_komp_4_d4_k4_keterlibatan_kelembagaan * 0.15 +
|
||||
$skorPetani->skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan * 0.10
|
||||
) / 0.70;
|
||||
|
||||
$skorPetani->skor_kesiapan_adopsi = (
|
||||
$skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar * 0.25 +
|
||||
$skorPetani->skor_komp_2_d4_k2_ekonomi_rumah_tangga * 0.20 +
|
||||
$skorPetani->skor_komp_3_d2_praktik_budidaya_gap * 0.20 +
|
||||
$skorPetani->skor_komp_4_d4_k4_keterlibatan_kelembagaan * 0.15 +
|
||||
$skorPetani->skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan * 0.10 +
|
||||
$skorPetani->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi * 0.10
|
||||
$skorPetani->skor_komp_1_d4_k3_motivasi_kapasitas_adopsi * 0.25 + // D4
|
||||
$skorPetani->skor_komp_2_d4_k2_ekonomi_rumah_tangga * 0.20 + // D4
|
||||
$skorPetani->skor_komp_3_d2_praktik_budidaya_gap * 0.20 + // D2
|
||||
$skorPetani->skor_komp_4_d4_k4_keterlibatan_kelembagaan * 0.15 + // D4
|
||||
$skorPetani->skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan * 0.10 + // D4
|
||||
$skorPetani->skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar * 0.10 // D6
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function petani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Petani::class, 'petani_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use App\Enums\SosialEkonomi\AlasanKreditDitolak;
|
||||
use App\Enums\SosialEkonomi\BisaBeliPupukBersubsidi;
|
||||
use App\Enums\SosialEkonomi\JenisAsetProduktif;
|
||||
@@ -41,6 +42,7 @@ use App\Enums\SosialEkonomi\UsahaMutuTambahanHarga;
|
||||
|
||||
use App\Models\Pertanian\Petani;
|
||||
use App\Models\Pertanian\SkorPetani;
|
||||
use App\Models\Pertanian\PraktikBudidaya;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -50,7 +52,7 @@ use Illuminate\Support\Str;
|
||||
|
||||
class SosialEkonomi extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'sosial_ekonomi';
|
||||
|
||||
@@ -204,7 +206,42 @@ class SosialEkonomi extends Model
|
||||
'updated_by' => $petani->updated_by
|
||||
]);
|
||||
}
|
||||
$praktik = PraktikBudidaya::where('petani_id', $petani->id)->orderBy('id')->first();
|
||||
$pendapatan = PendapatanKomoditas::where('sosial_ekonomi_id', $sosialEkonomi->id)->get();
|
||||
|
||||
$rasio = 0;
|
||||
if ($praktik && ($pendapatan && $pendapatan->count() > 0 && $pendapatan->sum('pendapatan') > 0)) {
|
||||
$rasio = $praktik->total_estimasi_biaya_produksi / $pendapatan->sum('pendapatan');
|
||||
}
|
||||
if ($rasio >= 0.8) {
|
||||
$skorRasio = 1;
|
||||
} else if ($rasio >= 0.6) {
|
||||
$skorRasio = 2;
|
||||
} else if ($rasio >= 0.45) {
|
||||
$skorRasio = 3;
|
||||
} else if ($rasio >= 0.3) {
|
||||
$skorRasio = 4;
|
||||
} else {
|
||||
$skorRasio = 5;
|
||||
}
|
||||
|
||||
$skor->update([
|
||||
'skor_d4_k3_1_praktik_baru' => $sosialEkonomi->kemauan_mencoba_praktik_baru?->value,
|
||||
'skor_d4_k3_4_pemahaman_mutu_harga' => $sosialEkonomi->pemahaman_mutu_harga?->value,
|
||||
'skor_d4_k3_5_role_model' => $sosialEkonomi->panutan_petani_sukses?->value,
|
||||
'skor_d4_k3_6_kemampuan_keterlibatan_program' => $sosialEkonomi->kemauan_ikut_program?->value,
|
||||
'skor_d4_k3_7_orientasi_jangka_panjang' => $sosialEkonomi->orientasi_rencana_jangka_panjang?->value,
|
||||
|
||||
'skor_d4_k2_5_stabilitas_penghasilan' => $sosialEkonomi->skor_periode_paceklik?->value,
|
||||
'skor_d4_k2_8_kemampuan_menabung' => $sosialEkonomi->skor_kemampuan_menabung?->value,
|
||||
'skor_d4_k2_9_kecukupan_penghasilan' => $sosialEkonomi->skor_kecukupan_penghasilan?->value,
|
||||
'skor_d4_k2_3_k2_7_rasio_biaya_produksi_pendapatan' => $skorRasio,
|
||||
'skor_d4_k2_11_hak_kelola_lahan' => $sosialEkonomi->skor_kejelasan_status_lahan?->value,
|
||||
|
||||
'skor_d4_k4_2_keaktifan_pertemuan' => $sosialEkonomi->keaktifan_pertemuan_kelompok?->value,
|
||||
'skor_d4_k4_3_peran_dalam_kelompok' => $sosialEkonomi->peran_dalam_kelompok?->value,
|
||||
'skor_d4_k4_4_manfaat_dirasakan' => $sosialEkonomi->manfaat_kelompok_tani?->value,
|
||||
|
||||
'skor_d4_k5_1_kepemilikan_rekening' => $sosialEkonomi->status_rekening_tabungan?->value,
|
||||
'skor_d4_k5_2_pengalaman_kredit_formal' => $sosialEkonomi->pengalaman_kredit_kebun?->value,
|
||||
'skor_d4_k5_3_kepemilikan_aset_produktif' => $sosialEkonomi->kondisi_aset_produktif?->value,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -11,7 +12,7 @@ use App\Models\Pertanian\Petani;
|
||||
|
||||
class SurveyPanen extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'survey_panen';
|
||||
|
||||
@@ -44,6 +45,33 @@ class SurveyPanen extends Model
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(function (SurveyPanen $survey) {
|
||||
|
||||
$petani = Petani::find($survey->petani_id);
|
||||
$skor = SkorPetani::where('petani_id', $petani->id)->orderBy('id')->first();
|
||||
if (!$skor) {
|
||||
$petaniUid = $petani->uid;
|
||||
$part = explode('-', $petaniUid);
|
||||
$skor = SkorPetani::create([
|
||||
'petani_id' => $petani->id,
|
||||
'uid' => 'SKR-' . $part[1] . '-' . strtoupper(Str::random(10)),
|
||||
'created_by' => $petani->created_by,
|
||||
'updated_by' => $petani->updated_by
|
||||
]);
|
||||
}
|
||||
|
||||
$skor->update([
|
||||
'skor_d2_k3_1_penentuan_pematangan' => $survey->dasar_penentuan_panen,
|
||||
'skor_d2_k3_5_sortasi_awal_pemisahan_kualitas' => $survey->praktik_sortasi,
|
||||
// Hambatan mutu = "Tingkat hasil panen ditolak pembeli" (TingkatPanenReject, 1–5).
|
||||
// Kolomnya bernama hasil_panen_reject, tetapi isinya tingkat reject.
|
||||
'skor_d6_k5_1_hambatan_mutu' => $survey->hasil_panen_reject,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function petani(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Petani::class, 'petani_id');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -11,7 +12,7 @@ use App\Models\Pertanian\Petani;
|
||||
|
||||
class SurveyPelatihan extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'survey_pelatihan';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SurveyProduktivitas extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'survey_produktivitas';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SurveySertifikasi extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'survey_sertifikasi';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Pertanian;
|
||||
|
||||
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Models\Profile;
|
||||
@@ -17,7 +18,7 @@ use App\Enums\KeparahanSerangan;
|
||||
|
||||
class Tanaman extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||
|
||||
protected $table = 'map_tanaman';
|
||||
|
||||
@@ -42,6 +43,9 @@ class Tanaman extends Model
|
||||
'serangan_hama',
|
||||
'penyakit_tanaman',
|
||||
'keparahan_serangan',
|
||||
|
||||
'jenis_hama',
|
||||
'jenis_penyakit',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -49,6 +53,8 @@ class Tanaman extends Model
|
||||
'serangan_hama' => SeranganHama::class,
|
||||
'penyakit_tanaman' => PenyakitTanaman::class,
|
||||
'keparahan_serangan' => KeparahanSerangan::class,
|
||||
'jenis_hama' => 'array',
|
||||
'jenis_penyakit' => 'array'
|
||||
];
|
||||
|
||||
public function komoditas()
|
||||
@@ -75,4 +81,11 @@ class Tanaman extends Model
|
||||
{
|
||||
return $this->belongsTo(Lahan::class, 'lahan_id');
|
||||
}
|
||||
|
||||
// Untuk trait KembalikanStatusPetaniKeDraf: petani diambil dari lahannya.
|
||||
protected function petaniIdUntukStatus(): ?int
|
||||
{
|
||||
return $this->petani_id ?? self::ingatPerRequest('status_petani.lahan', $this->lahan_id,
|
||||
fn () => Lahan::withoutGlobalScopes()->whereKey($this->lahan_id)->value('petani_id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
use App\Models\Konfigurasi\KoordinatorKomoditas;
|
||||
use App\Models\Pertanian\Petani;
|
||||
|
||||
class User extends Authenticatable
|
||||
@@ -52,4 +53,42 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(Petani::class, 'created_by');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::deleting(function (User $user) {
|
||||
if ($user->isForceDeleting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->koordinatorKomoditas()->get()->each->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function koordinatorKomoditas(): HasMany
|
||||
{
|
||||
return $this->hasMany(KoordinatorKomoditas::class, 'user_id');
|
||||
}
|
||||
|
||||
public function isKoordinator(): bool
|
||||
{
|
||||
return $this->role === Role::KOORDINATOR;
|
||||
}
|
||||
|
||||
// Daftar id komoditas yang ditugaskan ke koordinator ini.
|
||||
public function komoditasKoordinatorIds(): array
|
||||
{
|
||||
return $this->koordinatorKomoditas()->pluck('komoditas_id')->all();
|
||||
}
|
||||
|
||||
// Koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan.
|
||||
// Role lain tidak dibatasi di sini (aturannya tetap di masing-masing controller).
|
||||
public function bolehAksesPetani(Petani $petani): bool
|
||||
{
|
||||
if (! $this->isKoordinator()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($petani->komoditas_id, $this->komoditasKoordinatorIds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ class AppServiceProvider extends ServiceProvider
|
||||
return $user->role === Role::ADMIN;
|
||||
});
|
||||
|
||||
Gate::define('koordinator', function (User $user) {
|
||||
return $user->role === Role::KOORDINATOR;
|
||||
});
|
||||
|
||||
Gate::define('enumerator', function (User $user) {
|
||||
return $user->role === Role::ENUMERATOR;
|
||||
});
|
||||
|
||||
Gate::define('fasilitator', function (User $user) {
|
||||
return $user->role === Role::FASILITATOR;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
|
||||
class AlphaVantageMarket
|
||||
{
|
||||
public function quotes(Repository $cache): array
|
||||
{
|
||||
if (!$key = config('services.alpha_vantage.api_key')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Retain existing cache keys so cleanup does not spend another API request.
|
||||
$cacheKey = 'market.additional.v1.alpha:COFFEE';
|
||||
$lastKey = 'market.additional.last.v1.alpha:COFFEE';
|
||||
if (is_array($quotes = $cache->get($cacheKey))) {
|
||||
return $quotes;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::acceptJson()
|
||||
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||
->connectTimeout(3)->timeout(6)
|
||||
->get('https://www.alphavantage.co/query', [
|
||||
'function' => 'COFFEE', 'interval' => 'monthly', 'apikey' => $key,
|
||||
])->throw();
|
||||
$data = $response->json();
|
||||
if (!is_array($data) || ($data['interval'] ?? null) !== 'monthly'
|
||||
|| strtolower($data['unit'] ?? '') !== 'cents per pound' || !is_array($data['data'] ?? null)) {
|
||||
throw new \RuntimeException('Invalid Alpha Vantage response');
|
||||
}
|
||||
$latest = null;
|
||||
foreach ($data['data'] as $row) {
|
||||
if (!is_array($row) || !is_numeric($row['value'] ?? null) || !is_finite((float) $row['value']) || $row['value'] <= 0
|
||||
|| !is_string($row['date'] ?? null) || !preg_match('/^\d{4}-(0[1-9]|1[0-2])-01$/', $row['date'])) {
|
||||
continue;
|
||||
}
|
||||
if (!$latest || $row['date'] > $latest['date']) $latest = $row;
|
||||
}
|
||||
if (!$latest) throw new \RuntimeException('No valid Alpha Vantage price');
|
||||
$quote = [
|
||||
'id' => 'alpha:COFFEE', 'name' => 'Kopi Arabica',
|
||||
'source' => 'Alpha Vantage (data IMF via FRED)',
|
||||
'source_url' => 'https://www.alphavantage.co/documentation/#coffee',
|
||||
'price' => (float) $latest['value'], 'currency' => 'USX', 'unit' => 'sen AS / lb',
|
||||
'period' => substr($latest['date'], 0, 7), 'frequency' => 'monthly', 'stale' => false,
|
||||
'description' => 'Biji kopi Arabica (Other Milds). Estimasi konversi harga acuan rata-rata bulanan IMF melalui Alpha Vantage.',
|
||||
'update_info' => 'Data Alpha Vantage diperiksa setiap hari untuk menjaga kuota API. Harga merupakan rata-rata bulanan dari IMF via FRED.',
|
||||
];
|
||||
$cache->put($lastKey, $quote, now()->addDays(30));
|
||||
$cache->put($cacheKey, [$quote], 86400);
|
||||
return [$quote];
|
||||
} catch (Throwable) {
|
||||
// Request exceptions can contain the API key in their URL; do not log them.
|
||||
$quotes = [];
|
||||
if (is_array($last = $cache->get($lastKey))) {
|
||||
$last['stale'] = true;
|
||||
$quotes[] = $last;
|
||||
}
|
||||
$cache->put($cacheKey, $quotes, 3600);
|
||||
return $quotes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Produktivitas;
|
||||
|
||||
use App\Enums\AksesPasar\PraktikFermentasi;
|
||||
use App\Enums\PraktikBudidaya\MetodePenangananHamaPenyakit;
|
||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||
use App\Enums\SosialEkonomi\TerdaftarKelompokTani;
|
||||
use App\Models\Pertanian\Lahan;
|
||||
use App\Models\Pertanian\Petani;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Daftar isian Profil Petani yang WAJIB (dipakai skor Kesiapan Adopsi)
|
||||
* dan PERLU (dipakai dashboard Karakteristik, Produksi, Pasar).
|
||||
*
|
||||
* Satu-satunya tempat daftar ini ditulis. Dipakai oleh ringkasan kelengkapan
|
||||
* di atas setiap tab petani, dan nanti oleh fitur verifikasi.
|
||||
*
|
||||
* Isian yang tidak berlaku (mis. jenis hama kalau "tidak ada hama",
|
||||
* data kelompok kalau bukan anggota) tidak dimasukkan.
|
||||
*/
|
||||
class KelengkapanPetani
|
||||
{
|
||||
public const WAJIB = 'wajib';
|
||||
public const PERLU = 'perlu';
|
||||
|
||||
public const TAB_LABEL = [
|
||||
'profil' => 'Profil',
|
||||
'lahan' => 'Lahan & Tanaman',
|
||||
'praktik-budidaya' => 'Praktik Budidaya',
|
||||
'sosial-ekonomi' => 'Sosial Ekonomi',
|
||||
'akses-pasar' => 'Akses Pasar',
|
||||
];
|
||||
|
||||
private const KONSERVASI_TIDAK_ADA = 'Tidak ada yang seperti itu di kebun saya';
|
||||
|
||||
/*
|
||||
* Seluruh isian Profil Petani (total 207 untuk 1 lahan), per tabel.
|
||||
* Aturan hitung: satu pertanyaan = satu isian; kolom repeater dihitung sekali per kolom;
|
||||
* teks "Lainnya" tidak dihitung; provinsi/kabupaten/kecamatan ikut terisi bila desa terisi.
|
||||
*/
|
||||
private const ISIAN_PROFIL = [ // 15 + 3 wilayah = 18
|
||||
'nama', 'nomor_telepon', 'alamat', 'kk', 'ktp', 'path_kk', 'path_ktp', 'foto',
|
||||
'jenis_kelamin', 'tempat_lahir', 'tanggal_lahir', 'pendidikan_terakhir', 'status_perkawinan',
|
||||
'komoditas_id', 'desa_kelurahan_id',
|
||||
];
|
||||
|
||||
private const ISIAN_LAHAN = [ // 16 + 3 wilayah = 19, per lahan
|
||||
'desa_kelurahan_id', 'path', 'luas_lahan', 'komoditas_id', 'jenis_komoditas_id', 'varietas_id',
|
||||
'jumlah_tanaman', 'kondisi_tanaman', 'keparahan_serangan',
|
||||
'jenis_hama', 'jenis_penyakit', 'umur_tanaman', 'umur_lahan', 'komoditas_lain', 'tanaman_penaung', 'foto',
|
||||
];
|
||||
|
||||
private const ISIAN_PRAKTIK_BUDIDAYA = [ // 39
|
||||
'tenaga_kerja', 'frekuensi_pemangkasan_utama', 'teknik_pemangkasan', 'frekuensi_pemangkasan_penaung',
|
||||
'penyerbukan_manual', 'deskripsi_penyerbukan_manual', 'tingkat_keberhasilan_penyerbukan',
|
||||
'pemberian_pupuk', 'jenis_pupuk', 'frekuensi_pemberian_pupuk', 'estimasi_dosis_pupuk_organik',
|
||||
'estimasi_dosis_pupuk_anorganik', 'waktu_pemberian_pupuk', 'cara_pemberian_pupuk',
|
||||
'metode_penanganan_opt', 'jenis_hama', 'jenis_penyakit', 'intensitas_serangan', 'waktu_kemunculan_terparah',
|
||||
'metode_paling_sering_dilakukan', 'volume_obat_obatan_padat_permusim', 'volume_obat_obatan_cair_permusim',
|
||||
'frekuensi_sanitasi_lahan', 'penanganan_sisa_buah_ranting', 'penanganan_gulma',
|
||||
'kondisi_pengelolaan_air', 'sumber_air', 'sistem_irigasi', 'kebutuhan_air_permusim',
|
||||
'kebiasaan_konservasi_tanah', 'pemahaman_manfaat_konservasi_tanah',
|
||||
'upah_tenaga_kerja', 'biaya_bibit', 'biaya_pupuk', 'biaya_obat_obatan', 'biaya_sewa_alat_produksi',
|
||||
'biaya_penyediaan_air', 'biaya_produksi_lainnya', 'total_estimasi_biaya_produksi',
|
||||
];
|
||||
|
||||
private const ISIAN_PANEN = [ // 21
|
||||
'dasar_penentuan_panen', 'frekuensi_panen_pertahun', 'bulan_panen_utama', 'hasil_komoditas_utama_permusim',
|
||||
'hasil_komoditas_lainnya_permusim', 'disimpan_untuk_sendiri', 'hasil_panen_disimpan', 'tujuan_disimpan',
|
||||
'praktik_sortasi', 'bentuk_sortasi', 'hasil_panen_reject', 'jumlah_panen_reject', 'tindakan_panen_reject',
|
||||
'upah_tenaga_panen', 'biaya_pengangkutan_panen', 'biaya_wadah_panen', 'biaya_sewa_alat_panen',
|
||||
'biaya_panen_lainnya', 'total_estimasi_biaya_panen', 'estimasi_kehilangan_akibat_opt',
|
||||
'estimasi_kehilangan_akibat_hama_penyakit',
|
||||
];
|
||||
|
||||
private const ISIAN_PRODUKTIVITAS = ['komoditas_id', 'kuantitas', 'satuan']; // 3 (repeater)
|
||||
private const ISIAN_PELATIHAN = ['jenis_pelatihan', 'penyelenggara', 'tahun_pelaksanaan']; // 3 (repeater)
|
||||
private const ISIAN_SERTIFIKASI = ['jenis_sertifikasi', 'lembaga_penerbit', 'tahun_pelaksanaan', 'is_aktif']; // 4 (repeater)
|
||||
|
||||
private const ISIAN_SOSIAL_EKONOMI = [ // 70 + 4 bantuan NGO + 4 pendapatan komoditas + 2 pendapatan luar = 80
|
||||
'jumlah_tanggungan', 'anak_masih_sekolah', 'yang_ikut_bekerja', 'skor_periode_paceklik', 'bulan_paceklik',
|
||||
'sumber_penghasilan_paceklik', 'total_estimasi_biaya_kebun_per_musim', 'skor_kemampuan_menabung',
|
||||
'estimasi_tabungan_per_bulan', 'skor_kecukupan_penghasilan', 'alasan_kecukupan_penghasilan',
|
||||
'status_kepemilikan_lahan', 'skor_kejelasan_status_lahan', 'ada_sertifikat_dokumen', 'ada_sengketa',
|
||||
'keterangan_sengketa', 'pihak_membantu_konflik', 'pihak_membantu_konflik_nama', 'tahun_bantuan_konflik',
|
||||
'status_konflik', 'kemauan_mencoba_praktik_baru', 'alasan_mencoba_praktik_baru',
|
||||
'hambatan_mengubah_cara_bertani', 'pemahaman_mutu_harga', 'contoh_pemahaman_mutu_harga',
|
||||
'pengetahuan_harga_hasil_olah', 'pernah_jual_harga_lebih_tinggi', 'penyebab_harga_lebih_tinggi',
|
||||
'usaha_mutu_tambahan_harga', 'alasan_usaha_mutu_tambahan_harga', 'panutan_petani_sukses',
|
||||
'nama_lokasi_panutan', 'kemauan_ikut_program', 'harapan_program', 'orientasi_rencana_jangka_panjang',
|
||||
'rencana_jangka_panjang', 'terdaftar_kelompok_tani', 'nama_kelompok_tani', 'lama_bergabung_kelompok_tahun',
|
||||
'kedekatan_sesama_anggota', 'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok',
|
||||
'manfaat_kelompok_tani', 'status_kartu_tani', 'bisa_beli_pupuk_bersubsidi', 'manfaat_lain_kartu_tani',
|
||||
'status_rekening_tabungan', 'nama_bank', 'pengalaman_kredit_kebun', 'alasan_kredit_ditolak',
|
||||
'kondisi_aset_produktif', 'jenis_aset_produktif', 'keterangan_kondisi_aset',
|
||||
'keterlibatan_program_keuangan_formal', 'status_program_keuangan_formal', 'pernah_bantuan_ngo_perusahaan',
|
||||
'keterikatan_hutang_panen', 'pinjaman_tokoh_adat_panen', 'keterangan_pinjaman_tokoh_adat',
|
||||
'total_hutang_rupiah', 'total_hutang_setara_kg_panen', 'total_hutang_tidak_bersedia', 'praktik_ijon',
|
||||
'tekanan_jual_segera', 'penyebab_tekanan_jual_segera', 'kebebasan_memilih_pembeli',
|
||||
'alasan_tidak_bebas_memilih_pembeli', 'minat_pembeli_harga_stabil', 'alasan_minat_pembeli_harga_stabil',
|
||||
];
|
||||
private const ISIAN_BANTUAN_NGO = ['nama_lembaga', 'bentuk_bantuan', 'frekuensi', 'tahun_terakhir'];
|
||||
private const ISIAN_PENDAPATAN_KOMODITAS = ['kategori', 'komoditas', 'pendapatan', 'periode'];
|
||||
private const ISIAN_PENDAPATAN_LUAR = ['jenis_pekerjaan', 'estimasi_per_bulan'];
|
||||
|
||||
private const ISIAN_AKSES_PASAR = [ // 16 + 4 harga jual = 20
|
||||
'tujuan_penjualan', 'sifat_hubungan_bisnis', 'bentuk_penjualan', 'frekuensi_penjualan', 'skor_pengalaman',
|
||||
'saluran_terdahulu', 'alasan_berhenti', 'tahu_harga_sebelumnya', 'sumber_info_harga', 'praktik_fermentasi',
|
||||
'proses_fermentasi', 'model_fermentasi_memungkinkan', 'skor_kesadaran_premium', 'sumber_info_premium',
|
||||
'tingkat_kepercayaan_pembeli_baru', 'alasan_kepercayaan_pembeli_baru',
|
||||
];
|
||||
private const ISIAN_HARGA_JUAL = ['bentuk_produk', 'harga_panen_terakhir', 'harga_tahun_lalu', 'harga_dua_tahun_lalu'];
|
||||
|
||||
/**
|
||||
* Satu instance per objek Petani, supaya header dan ringkasan tidak memuat/menghitung ulang.
|
||||
* WeakMap: otomatis dibuang saat objek Petani tidak dipakai lagi (tidak menumpuk antar-request).
|
||||
*/
|
||||
private static ?\WeakMap $cache = null;
|
||||
|
||||
private ?Collection $semuaKosong = null;
|
||||
private ?array $hitungIsian = null;
|
||||
|
||||
public function __construct(private Petani $petani)
|
||||
{
|
||||
$petani->loadMissing([
|
||||
'komoditas',
|
||||
'lahans',
|
||||
'praktikBudidaya',
|
||||
'surveyPanen',
|
||||
'surveyProduktivitas',
|
||||
'surveyPelatihan',
|
||||
'surveySertifikasi',
|
||||
'sosialEkonomi.pendapatanKomoditas',
|
||||
'sosialEkonomi.pendapatanLuarPertanian',
|
||||
'aksesPasar',
|
||||
'hargaJualPanen',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function untuk(Petani $petani): self
|
||||
{
|
||||
self::$cache ??= new \WeakMap();
|
||||
|
||||
return self::$cache[$petani] ??= new self($petani);
|
||||
}
|
||||
|
||||
/** Jumlah isian WAJIB (skor) yang masih kosong di semua tab. */
|
||||
public function jumlahWajibKosong(): int
|
||||
{
|
||||
return $this->semuaKosong()->flatten(1)->where('prioritas', self::WAJIB)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitungan seluruh isian: terisi, total (207 untuk 1 lahan; +19 per lahan tambahan),
|
||||
* tidak_berlaku (pertanyaan lanjutan yang tidak perlu dijawab), berlaku, persen.
|
||||
*/
|
||||
public function hitungIsian(): array
|
||||
{
|
||||
return $this->hitungIsian ??= $this->hitungSemuaIsian();
|
||||
}
|
||||
|
||||
private function hitungSemuaIsian(): array
|
||||
{
|
||||
$hasil = ['terisi' => 0, 'total' => 0, 'tidak_berlaku' => 0];
|
||||
$p = $this->petani;
|
||||
|
||||
// Profil (+ provinsi/kabupaten/kecamatan yang mengikuti desa)
|
||||
$this->hitungKolom($hasil, $p, self::ISIAN_PROFIL);
|
||||
$this->hitungNilai($hasil, 3, self::terisi($p->desa_kelurahan_id));
|
||||
|
||||
// Lahan & Tanaman, per lahan. Belum ada lahan = dihitung 1 lahan kosong.
|
||||
$daftarLahan = $p->lahans->isEmpty() ? collect([null]) : $p->lahans;
|
||||
foreach ($daftarLahan as $lahan) {
|
||||
$this->hitungKolom($hasil, $lahan, self::ISIAN_LAHAN);
|
||||
$this->hitungNilai($hasil, 3, self::terisi($lahan?->desa_kelurahan_id));
|
||||
}
|
||||
|
||||
// Praktik Budidaya, Panen, Produktivitas
|
||||
$pb = $p->praktikBudidaya;
|
||||
$tidakBerlakuPb = [];
|
||||
if (! $p->isKomoditasVanili()) {
|
||||
array_push($tidakBerlakuPb, 'penyerbukan_manual', 'deskripsi_penyerbukan_manual', 'tingkat_keberhasilan_penyerbukan');
|
||||
}
|
||||
if ((int) $pb?->pemberian_pupuk === PemberianPupuk::TIDAK_PERNAH->value) {
|
||||
array_push($tidakBerlakuPb, 'jenis_pupuk', 'frekuensi_pemberian_pupuk', 'estimasi_dosis_pupuk_organik',
|
||||
'estimasi_dosis_pupuk_anorganik', 'waktu_pemberian_pupuk', 'cara_pemberian_pupuk');
|
||||
}
|
||||
if ((int) $pb?->metode_penanganan_opt === MetodePenangananHamaPenyakit::TIDAK_ADA->value) {
|
||||
array_push($tidakBerlakuPb, 'jenis_hama', 'jenis_penyakit');
|
||||
}
|
||||
$this->hitungKolom($hasil, $pb, self::ISIAN_PRAKTIK_BUDIDAYA, $tidakBerlakuPb);
|
||||
$this->hitungKolom($hasil, $p->surveyPanen, self::ISIAN_PANEN);
|
||||
$this->hitungRepeater($hasil, $p->surveyProduktivitas, self::ISIAN_PRODUKTIVITAS);
|
||||
|
||||
// Pendidikan & Pelatihan
|
||||
$this->hitungRepeater($hasil, $p->surveyPelatihan, self::ISIAN_PELATIHAN);
|
||||
$this->hitungRepeater($hasil, $p->surveySertifikasi, self::ISIAN_SERTIFIKASI);
|
||||
|
||||
// Sosial Ekonomi (data kelompok tidak berlaku kalau bukan anggota)
|
||||
$se = $p->sosialEkonomi;
|
||||
$tidakBerlakuSe = self::nilaiEnum($se?->terdaftar_kelompok_tani) === TerdaftarKelompokTani::TIDAK->value
|
||||
? ['nama_kelompok_tani', 'lama_bergabung_kelompok_tahun', 'kedekatan_sesama_anggota',
|
||||
'keaktifan_pertemuan_kelompok', 'topik_biasa_dibahas', 'peran_dalam_kelompok', 'manfaat_kelompok_tani']
|
||||
: [];
|
||||
$this->hitungKolom($hasil, $se, self::ISIAN_SOSIAL_EKONOMI, $tidakBerlakuSe);
|
||||
$this->hitungRepeater($hasil, collect($se?->detail_bantuan_ngo_perusahaan ?? []), self::ISIAN_BANTUAN_NGO);
|
||||
$this->hitungRepeater($hasil, $se?->pendapatanKomoditas ?? collect(), self::ISIAN_PENDAPATAN_KOMODITAS);
|
||||
$this->hitungRepeater($hasil, $se?->pendapatanLuarPertanian ?? collect(), self::ISIAN_PENDAPATAN_LUAR);
|
||||
|
||||
// Akses Pasar (detail fermentasi tidak berlaku kalau belum pernah)
|
||||
$ap = $p->aksesPasar;
|
||||
$tidakBerlakuAp = self::nilaiEnum($ap?->praktik_fermentasi) === PraktikFermentasi::BELUM_PERNAH->value
|
||||
? ['proses_fermentasi', 'model_fermentasi_memungkinkan']
|
||||
: [];
|
||||
$this->hitungKolom($hasil, $ap, self::ISIAN_AKSES_PASAR, $tidakBerlakuAp);
|
||||
$this->hitungRepeater($hasil, $p->hargaJualPanen, self::ISIAN_HARGA_JUAL);
|
||||
|
||||
$hasil['berlaku'] = $hasil['total'] - $hasil['tidak_berlaku'];
|
||||
$hasil['persen'] = $hasil['berlaku'] > 0 ? (int) floor($hasil['terisi'] / $hasil['berlaku'] * 100) : 0;
|
||||
|
||||
return $hasil;
|
||||
}
|
||||
|
||||
private static function nilaiEnum(mixed $nilai): mixed
|
||||
{
|
||||
return $nilai instanceof \BackedEnum ? $nilai->value : $nilai;
|
||||
}
|
||||
|
||||
private function hitungKolom(array &$hasil, ?object $model, array $kolom, array $tidakBerlaku = []): void
|
||||
{
|
||||
foreach ($kolom as $k) {
|
||||
$hasil['total']++;
|
||||
|
||||
if (in_array($k, $tidakBerlaku, true)) {
|
||||
$hasil['tidak_berlaku']++;
|
||||
} elseif ($model && self::terisi($model->{$k})) {
|
||||
$hasil['terisi']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function hitungNilai(array &$hasil, int $jumlah, bool $terisi): void
|
||||
{
|
||||
$hasil['total'] += $jumlah;
|
||||
$hasil['terisi'] += $terisi ? $jumlah : 0;
|
||||
}
|
||||
|
||||
/** Kolom repeater dianggap terisi kalau minimal satu baris mengisinya. */
|
||||
private function hitungRepeater(array &$hasil, Collection $baris, array $kolom): void
|
||||
{
|
||||
foreach ($kolom as $k) {
|
||||
$hasil['total']++;
|
||||
$hasil['terisi'] += $baris->contains(fn ($row) => self::terisi(data_get($row, $k))) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Isian yang masih kosong di satu tab.
|
||||
* Setiap item: label, prioritas (wajib/perlu), input (atribut name di form),
|
||||
* grup (judul pengelompokan, mis. nama lahan), lahan_id, url (opsional, halaman lain).
|
||||
*/
|
||||
public function kosong(string $tab): Collection
|
||||
{
|
||||
return $this->semuaKosong()->get($tab, collect());
|
||||
}
|
||||
|
||||
private function hitungKosong(string $tab): Collection
|
||||
{
|
||||
$isian = match ($tab) {
|
||||
'profil' => $this->profil(),
|
||||
'lahan' => $this->lahan(),
|
||||
'praktik-budidaya' => $this->praktikBudidaya(),
|
||||
'sosial-ekonomi' => $this->sosialEkonomi(),
|
||||
'akses-pasar' => $this->aksesPasar(),
|
||||
default => [],
|
||||
};
|
||||
|
||||
return collect($isian)
|
||||
->reject(fn (array $item) => $item['terisi'])
|
||||
->map(fn (array $item) => collect($item)->except('terisi')->all())
|
||||
->values();
|
||||
}
|
||||
|
||||
/** Isian kosong semua tab, dikelompokkan per tab (untuk tab Kesiapan Adopsi). */
|
||||
public function semuaKosong(): Collection
|
||||
{
|
||||
return $this->semuaKosong ??= collect(array_keys(self::TAB_LABEL))
|
||||
->mapWithKeys(fn (string $tab) => [$tab => $this->hitungKosong($tab)]);
|
||||
}
|
||||
|
||||
private function item(string $label, string $prioritas, string $input, mixed $nilai, array $tambahan = []): array
|
||||
{
|
||||
return $this->itemCek($label, $prioritas, $input, self::terisi($nilai), $tambahan);
|
||||
}
|
||||
|
||||
/** Untuk isian yang status terisinya dihitung sendiri (mis. repeater). */
|
||||
private function itemCek(string $label, string $prioritas, string $input, bool $terisi, array $tambahan = []): array
|
||||
{
|
||||
return [
|
||||
'label' => $label,
|
||||
'prioritas' => $prioritas,
|
||||
'input' => $input,
|
||||
'terisi' => $terisi,
|
||||
'grup' => null,
|
||||
'lahan_id' => null,
|
||||
'url' => null,
|
||||
...$tambahan,
|
||||
];
|
||||
}
|
||||
|
||||
private static function terisi(mixed $nilai): bool
|
||||
{
|
||||
if ($nilai instanceof Collection) {
|
||||
return $nilai->isNotEmpty();
|
||||
}
|
||||
|
||||
return ! (is_null($nilai) || $nilai === '' || $nilai === []);
|
||||
}
|
||||
|
||||
private function profil(): array
|
||||
{
|
||||
$p = $this->petani;
|
||||
|
||||
return [
|
||||
$this->item('Jenis kelamin', self::PERLU, 'jenis_kelamin', $p->jenis_kelamin),
|
||||
$this->item('Tanggal lahir', self::PERLU, 'tanggal_lahir', $p->tanggal_lahir),
|
||||
$this->item('Pendidikan terakhir', self::PERLU, 'pendidikan_terakhir', $p->pendidikan_terakhir),
|
||||
$this->item('Komoditas utama', self::PERLU, 'komoditas_id', $p->komoditas_id),
|
||||
$this->item('Desa/kelurahan', self::PERLU, 'desa_kelurahan_id', $p->desa_kelurahan_id),
|
||||
];
|
||||
}
|
||||
|
||||
/** Diisi per lahan; setiap item membawa link ke halaman edit lahannya. */
|
||||
private function lahan(): array
|
||||
{
|
||||
$daftarLahan = $this->petani->lahans->sortBy('id');
|
||||
|
||||
if ($daftarLahan->isEmpty()) {
|
||||
return [$this->itemCek('Belum ada lahan', self::PERLU, '', false, [
|
||||
'url' => route('petani.lahan-tanaman.create', $this->petani->id),
|
||||
])];
|
||||
}
|
||||
|
||||
return $daftarLahan->values()->flatMap(function (Lahan $l, int $i) {
|
||||
$tambahan = [
|
||||
'grup' => $l->nama ?: 'Lahan ' . ($i + 1),
|
||||
'lahan_id' => $l->id,
|
||||
'url' => route('petani.lahan-tanaman.edit', [$this->petani->id, $l->id]),
|
||||
];
|
||||
// Jenis hama/penyakit lahan terisi otomatis dari Praktik Budidaya; kolom lama serangan_hama/penyakit_tanaman ikut dicek untuk data lama.
|
||||
$adaSerangan = self::terisi($l->jenis_hama) || self::terisi($l->jenis_penyakit)
|
||||
|| self::terisi($l->serangan_hama) || self::terisi($l->penyakit_tanaman);
|
||||
|
||||
return array_filter([
|
||||
$this->item('Luas lahan', self::PERLU, 'luas_lahan', $l->luas_lahan, $tambahan),
|
||||
$this->item('Peta/polygon lahan', self::PERLU, 'path', $l->path, $tambahan),
|
||||
$this->item('Desa lahan', self::PERLU, 'desa_kelurahan_id', $l->desa_kelurahan_id, $tambahan),
|
||||
$this->item('Umur tanaman', self::PERLU, 'umur_tanaman', $l->umur_tanaman, $tambahan),
|
||||
$this->item('Jumlah tanaman', self::PERLU, 'jumlah_tanaman', $l->jumlah_tanaman, $tambahan),
|
||||
$this->item('Kondisi tanaman', self::PERLU, 'kondisi_tanaman', $l->kondisi_tanaman, $tambahan),
|
||||
$adaSerangan
|
||||
? $this->item('Keparahan serangan', self::PERLU, 'keparahan_serangan', $l->keparahan_serangan, $tambahan)
|
||||
: null,
|
||||
]);
|
||||
})->all();
|
||||
}
|
||||
|
||||
private function praktikBudidaya(): array
|
||||
{
|
||||
$pb = $this->petani->praktikBudidaya;
|
||||
$panen = $this->petani->surveyPanen;
|
||||
|
||||
$adaHama = $pb && self::terisi($pb->metode_penanganan_opt)
|
||||
&& (int) $pb->metode_penanganan_opt !== MetodePenangananHamaPenyakit::TIDAK_ADA->value;
|
||||
|
||||
$konservasi = (array) ($pb?->kebiasaan_konservasi_tanah ?? []);
|
||||
$adaKonservasi = collect($konservasi)->contains(fn ($v) => $v !== self::KONSERVASI_TIDAK_ADA);
|
||||
|
||||
return array_filter([
|
||||
$this->item('Frekuensi pemangkasan', self::WAJIB, 'frekuensi_pemangkasan_komoditas_utama', $pb?->frekuensi_pemangkasan_utama),
|
||||
$this->item('Pemberian pupuk', self::WAJIB, 'pemberian_pupuk', $pb?->pemberian_pupuk),
|
||||
$this->item('Metode penanganan hama & penyakit', self::WAJIB, 'metode_penanganan_hama_penyakit', $pb?->metode_penanganan_opt),
|
||||
$adaHama ? $this->item('Jenis hama', self::PERLU, 'jenis_hama', $pb->jenis_hama) : null,
|
||||
$adaHama ? $this->item('Jenis penyakit', self::PERLU, 'jenis_penyakit', $pb->jenis_penyakit) : null,
|
||||
$this->item('Frekuensi sanitasi lahan', self::WAJIB, 'frekuensi_sanitasi_lahan', $pb?->frekuensi_sanitasi_lahan),
|
||||
$this->item('Penanganan gulma', self::WAJIB, 'penanganan_gulma', $pb?->penanganan_gulma),
|
||||
$this->item('Kondisi pengelolaan air & irigasi', self::WAJIB, 'kondisi_pengelolaan_air_irigasi', $pb?->kondisi_pengelolaan_air),
|
||||
$this->item('Sumber air', self::PERLU, 'sumber_air', $pb?->sumber_air),
|
||||
$this->item('Kebiasaan konservasi tanah', self::WAJIB, 'kebiasaan_konservasi_tanah', $konservasi),
|
||||
$adaKonservasi
|
||||
? $this->item('Pemahaman manfaat konservasi', self::WAJIB, 'pemahaman_manfaat_konservasi', $pb->pemahaman_manfaat_konservasi_tanah)
|
||||
: null,
|
||||
$this->item('Total estimasi biaya produksi', self::WAJIB, 'total_estimasi_biaya_produksi', $pb?->total_estimasi_biaya_produksi),
|
||||
$this->item('Dasar penentuan panen', self::WAJIB, 'dasar_penentuan_panen', $panen?->dasar_penentuan_panen),
|
||||
$this->item('Hasil komoditas utama per musim', self::PERLU, 'hasil_komoditas_utama_permusim', $panen?->hasil_komoditas_utama_permusim),
|
||||
$this->item('Praktik sortasi', self::WAJIB, 'praktik_sortasi', $panen?->praktik_sortasi),
|
||||
$this->item('Tingkat hasil panen ditolak pembeli', self::WAJIB, 'tingkat_reject', $panen?->hasil_panen_reject),
|
||||
]);
|
||||
}
|
||||
|
||||
private function sosialEkonomi(): array
|
||||
{
|
||||
$se = $this->petani->sosialEkonomi;
|
||||
$anggotaKelompok = $se?->terdaftar_kelompok_tani === TerdaftarKelompokTani::YA
|
||||
|| $se?->terdaftar_kelompok_tani === TerdaftarKelompokTani::YA->value;
|
||||
|
||||
$adaPendapatan = (bool) $se?->pendapatanKomoditas->contains(fn ($row) => (int) $row->pendapatan > 0);
|
||||
|
||||
return array_filter([
|
||||
// Utama
|
||||
$this->item('Jumlah tanggungan', self::PERLU, 'jumlah_tanggungan', $se?->jumlah_tanggungan),
|
||||
$this->itemCek('Pendapatan per komoditas', self::WAJIB, 'pendapatan_komoditas', $adaPendapatan),
|
||||
$this->item('Periode paceklik', self::WAJIB, 'skor_periode_paceklik', $se?->skor_periode_paceklik),
|
||||
$this->item('Kemampuan menabung', self::WAJIB, 'skor_kemampuan_menabung', $se?->skor_kemampuan_menabung),
|
||||
$this->item('Kecukupan penghasilan', self::WAJIB, 'skor_kecukupan_penghasilan', $se?->skor_kecukupan_penghasilan),
|
||||
$this->item('Kejelasan status lahan', self::WAJIB, 'skor_kejelasan_status_lahan', $se?->skor_kejelasan_status_lahan),
|
||||
|
||||
// Sosial budaya
|
||||
$this->item('Kemauan mencoba praktik baru', self::WAJIB, 'kemauan_mencoba_praktik_baru', $se?->kemauan_mencoba_praktik_baru),
|
||||
$this->item('Pemahaman mutu & harga', self::WAJIB, 'pemahaman_mutu_harga', $se?->pemahaman_mutu_harga),
|
||||
$this->item('Panutan petani sukses', self::WAJIB, 'panutan_petani_sukses', $se?->panutan_petani_sukses),
|
||||
$this->item('Kemauan ikut program', self::WAJIB, 'kemauan_ikut_program', $se?->kemauan_ikut_program),
|
||||
$this->item('Orientasi rencana jangka panjang', self::WAJIB, 'orientasi_rencana_jangka_panjang', $se?->orientasi_rencana_jangka_panjang),
|
||||
|
||||
// Keanggotaan kelompok: pertanyaan lanjutan hanya untuk anggota
|
||||
$this->item('Terdaftar di kelompok tani', self::WAJIB, 'terdaftar_kelompok_tani', $se?->terdaftar_kelompok_tani),
|
||||
$anggotaKelompok ? $this->item('Keaktifan pertemuan kelompok', self::WAJIB, 'keaktifan_pertemuan_kelompok', $se->keaktifan_pertemuan_kelompok) : null,
|
||||
$anggotaKelompok ? $this->item('Peran dalam kelompok', self::WAJIB, 'peran_dalam_kelompok', $se->peran_dalam_kelompok) : null,
|
||||
$anggotaKelompok ? $this->item('Manfaat kelompok tani', self::WAJIB, 'manfaat_kelompok_tani', $se->manfaat_kelompok_tani) : null,
|
||||
|
||||
// Akses keuangan
|
||||
$this->item('Status rekening tabungan', self::WAJIB, 'status_rekening_tabungan', $se?->status_rekening_tabungan),
|
||||
$this->item('Pengalaman kredit kebun', self::WAJIB, 'pengalaman_kredit_kebun', $se?->pengalaman_kredit_kebun),
|
||||
$this->item('Kondisi aset produktif', self::WAJIB, 'kondisi_aset_produktif', $se?->kondisi_aset_produktif),
|
||||
$this->item('Keterlibatan program keuangan formal', self::WAJIB, 'keterlibatan_program_keuangan_formal', $se?->keterlibatan_program_keuangan_formal),
|
||||
|
||||
// Keterikatan hutang
|
||||
$this->item('Keterikatan hutang panen', self::WAJIB, 'keterikatan_hutang_panen', $se?->keterikatan_hutang_panen),
|
||||
$this->item('Praktik ijon', self::WAJIB, 'praktik_ijon', $se?->praktik_ijon),
|
||||
$this->item('Kebebasan memilih pembeli', self::WAJIB, 'kebebasan_memilih_pembeli', $se?->kebebasan_memilih_pembeli),
|
||||
]);
|
||||
}
|
||||
|
||||
private function aksesPasar(): array
|
||||
{
|
||||
$ap = $this->petani->aksesPasar;
|
||||
$harga = $this->petani->hargaJualPanen;
|
||||
|
||||
return [
|
||||
$this->item('Tujuan penjualan', self::PERLU, 'tujuan_penjualan', $ap?->tujuan_penjualan),
|
||||
$this->item('Sifat hubungan dengan pembeli', self::PERLU, 'sifat_hubungan_bisnis', $ap?->sifat_hubungan_bisnis),
|
||||
$this->item('Frekuensi penjualan', self::PERLU, 'frekuensi_penjualan', $ap?->frekuensi_penjualan),
|
||||
$this->item('Pengalaman menjual ke saluran lain', self::WAJIB, 'skor_pengalaman', $ap?->skor_pengalaman),
|
||||
$this->item('Praktik fermentasi', self::PERLU, 'praktik_fermentasi', $ap?->praktik_fermentasi),
|
||||
$this->item('Kesadaran pasar premium', self::WAJIB, 'skor_kesadaran_premium', $ap?->skor_kesadaran_premium),
|
||||
$this->item('Kepercayaan pada pembeli baru', self::WAJIB, 'tingkat_kepercayaan_pembeli_baru', $ap?->tingkat_kepercayaan_pembeli_baru),
|
||||
$this->itemCek('Harga jual panen terakhir', self::PERLU, 'harga_jual',
|
||||
$harga->contains(fn ($h) => self::terisi($h->bentuk_produk) && (float) $h->harga_panen_terakhir > 0)),
|
||||
$this->itemCek('Harga jual tahun lalu', self::PERLU, 'harga_jual',
|
||||
$harga->contains(fn ($h) => (float) $h->harga_tahun_lalu > 0)),
|
||||
$this->itemCek('Harga jual dua tahun lalu', self::PERLU, 'harga_jual',
|
||||
$harga->contains(fn ($h) => (float) $h->harga_dua_tahun_lalu > 0)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->append(\App\Http\Middleware\ResolveProgram::class);
|
||||
|
||||
$middleware->alias([
|
||||
'akses.petani' => \App\Http\Middleware\BatasiAksesPetani::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -14,6 +14,12 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'alpha_vantage' => ['api_key' => env('ALPHA_VANTAGE_API_KEY')],
|
||||
|
||||
'yahoo_finance' => [
|
||||
'ca_bundle' => env('YAHOO_FINANCE_CA_BUNDLE'),
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
@@ -35,24 +35,25 @@ return new class extends Migration
|
||||
$table->string('cara_pemberian_pupuk')->nullable();
|
||||
// 2.5 Pengendalian Organisme Pengganggu Tumbuhan (OPT)
|
||||
$table->integer('metode_penanganan_opt')->nullable();
|
||||
$table->string('jenis_hama')->nullable();
|
||||
$table->string('jenis_penyakit')->nullable();
|
||||
$table->jsonb('jenis_hama')->nullable();
|
||||
$table->jsonb('jenis_penyakit')->nullable();
|
||||
$table->string('intensitas_serangan')->nullable();
|
||||
$table->string('waktu_kemunculan_terparah')->nullable();
|
||||
$table->string('metode_paling_sering_dilakukan')->nullable();
|
||||
$table->integer('volume_obat_obatan_permusim')->nullable();
|
||||
$table->decimal('volume_obat_obatan_padat_permusim', 10, 2)->nullable();
|
||||
$table->decimal('volume_obat_obatan_cair_permusim', 10, 2)->nullable();
|
||||
// 2.6 Sanitasi Kebun
|
||||
$table->integer('frekuensi_sanitasi_lahan')->nullable();
|
||||
$table->string('penanganan_sisa_buah_ranting')->nullable();
|
||||
$table->integer('penanganan_gulma')->nullable();
|
||||
// 2.7 Pengelolaan Air & irigasi
|
||||
$table->integer('kondisi_pengelolaan_air')->nullable();
|
||||
$table->string('sumber_air')->nullable();
|
||||
$table->jsonb('sumber_air')->nullable();
|
||||
$table->string('sistem_irigasi')->nullable();
|
||||
$table->string('kebutuhan_air_permusim')->nullable();
|
||||
|
||||
// 2.8 Praktik konservasi tanah
|
||||
$table->string('kebiasaan_konservasi_tanah')->nullable();
|
||||
$table->jsonb('kebiasaan_konservasi_tanah')->nullable();
|
||||
$table->string('pemahaman_manfaat_konservasi_tanah')->nullable();
|
||||
$table->integer('nilai_konservasi_tanah')->nullable();
|
||||
|
||||
@@ -99,7 +100,7 @@ return new class extends Migration
|
||||
$table->string('tindakan_panen_reject')->nullable();
|
||||
$table->decimal('upah_tenaga_panen', 13, 2)->nullable();
|
||||
$table->decimal('biaya_pengangkutan_panen', 13, 2)->nullable();
|
||||
$table->decimal('biaya_wadah-Panen', 13, 2)->nullable();
|
||||
$table->decimal('biaya_wadah_Panen', 13, 2)->nullable();
|
||||
$table->decimal('biaya_sewa_alat_panen', 13, 2)->nullable();
|
||||
$table->decimal('biaya_panen_lainnya', 13, 2)->nullable();
|
||||
$table->decimal('total_estimasi_biaya_panen', 13, 2)->nullable();
|
||||
|
||||
@@ -61,7 +61,7 @@ return new class extends Migration
|
||||
$table->integer('harga_tahun_lalu')->nullable();
|
||||
$table->integer('harga_dua_tahun_lalu')->nullable();
|
||||
|
||||
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table ->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('master_komoditas', function (Blueprint $table) {
|
||||
$table->boolean('is_utama')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('master_komoditas', function (Blueprint $table) {
|
||||
$table->dropColumn('is_utama');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('master_jenis_tanaman', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nama');
|
||||
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jenis_tanamen');
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user