Compare commits
45
Commits
1155b9b95b
..
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 |
@@ -63,3 +63,9 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
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
|
||||||
@@ -4,12 +4,13 @@ namespace App\Enums\PendidikanPelatihan;
|
|||||||
|
|
||||||
enum PelatihanGAP: string
|
enum PelatihanGAP: string
|
||||||
{
|
{
|
||||||
case PEMANGKASAN = 'Pemangkasan atau perawatan tanaman';
|
case PEMANGKASAN = 'Pemangkasan/perawatan tanaman';
|
||||||
case PEMUPUKAN = 'Pemupukan atau nutrisi tanah';
|
case PEMUPUKAN = 'Pemupukan/nutrisi';
|
||||||
case PENGENDALIAN = 'Pengendalian hama dan penyakit';
|
case PENGENDALIAN = 'Pengendalian hama & penyakit';
|
||||||
case PRAKTIK = 'Praktik Berkebun yang Baik (GAP)';
|
case PRAKTIK = 'Praktik Berkebun yang Baik (GAP)';
|
||||||
case PENGOLAHAN = 'Pengolahan/pascapanen';
|
case PENGOLAHAN = 'Pengolahan/pascapanen';
|
||||||
case PENYERBUKAN = 'Penyerbukan manual (Vanili)';
|
case PENYERBUKAN = 'Penyerbukan Manual (Vanili)';
|
||||||
|
case LAINNYA = 'Lainnya';
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ enum FrekuensiPemangkasanPendamping: string
|
|||||||
case TIDAK_RUTIN = 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya';
|
case TIDAK_RUTIN = 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya';
|
||||||
case SATU_KALI_TAHUN = 'Memangkas 1 kali/tahun, tahu jenis tekniknya';
|
case SATU_KALI_TAHUN = 'Memangkas 1 kali/tahun, tahu jenis tekniknya';
|
||||||
case DUA_KALI_TAHUN = 'Memangkas 2 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
|
public function label(): string
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ enum FrekuensiPemangkasanPendamping: string
|
|||||||
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
|
self::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
|
||||||
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
||||||
self::DUA_KALI_TAHUN => 'Memangkas 2 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::TIDAK_RUTIN => 'Memangkas, tapi tidak rutin dan tidak tahu tekniknya',
|
||||||
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
self::SATU_KALI_TAHUN => 'Memangkas 1 kali/tahun, tahu jenis tekniknya',
|
||||||
self::DUA_KALI_TAHUN => 'Memangkas 2 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,7 +16,7 @@ enum MetodePenangananHamaPenyakit: int
|
|||||||
self::TIDAK_ADA => 'Tidak ada penanganan',
|
self::TIDAK_ADA => 'Tidak ada penanganan',
|
||||||
self::CARA_TRADISIONAL => 'Cara tradisional (doa/ritual)',
|
self::CARA_TRADISIONAL => 'Cara tradisional (doa/ritual)',
|
||||||
self::PESTISIDA_SANITASI => 'Pestisida atau sanitasi',
|
self::PESTISIDA_SANITASI => 'Pestisida atau sanitasi',
|
||||||
self::KOMBINASI => 'Kombinasi pestisada dan sanitasi',
|
self::KOMBINASI => 'Kombinasi pestisida dan sanitasi',
|
||||||
self::PHT => 'Pengendalian hama terpadu (PHT)',
|
self::PHT => 'Pengendalian hama terpadu (PHT)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -6,7 +6,22 @@ enum Role: string
|
|||||||
{
|
{
|
||||||
case SUPER_ADMIN = 'super_admin';
|
case SUPER_ADMIN = 'super_admin';
|
||||||
case ADMIN = 'admin'; // admin program
|
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 PETANI = 'petani';
|
||||||
case OFFTAKER = 'offtaker';
|
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
|
public function index(Request $request, string $tabelId): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class TabelBasisDataController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): AnonymousResourceCollection
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class TabelBasisDataController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(Request $request, string $id): JsonResponse
|
public function show(Request $request, string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ValueBasisDataController extends Controller
|
|||||||
|
|
||||||
public function index(Request $request, string $tabelId): JsonResponse
|
public function index(Request $request, string $tabelId): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ class ValueBasisDataController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request, string $tabelId): JsonResponse
|
public function store(Request $request, string $tabelId): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ class ValueBasisDataController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $tabelId, string $id): JsonResponse
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ class ValueBasisDataController extends Controller
|
|||||||
|
|
||||||
public function destroy(Request $request, string $tabelId, string $id): JsonResponse
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class JenisKomoditasController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): AnonymousResourceCollection
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class JenisKomoditasController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class JenisKomoditasController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ class JenisKomoditasController extends Controller
|
|||||||
*/
|
*/
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class JenisKomoditasController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class DistributionController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(): JsonResponse
|
public function index(): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
|
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class DistributionController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
|
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class DistributionController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $id): JsonResponse
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class DistributionController extends Controller
|
|||||||
|
|
||||||
public function batchUpsert(Request $request): JsonResponse
|
public function batchUpsert(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'petani','fasilitator', 'offtaker'])) {
|
if (!Gate::any(['super_admin', 'petani','enumerator', 'offtaker'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class LahanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): AnonymousResourceCollection
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class LahanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ class LahanController extends Controller
|
|||||||
$request->merge(['profile_id' => (int) $request->profile_id]);
|
$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([
|
$validated = $request->validate([
|
||||||
'nama' => ['required', 'string', 'max:255'],
|
'nama' => ['required', 'string', 'max:255'],
|
||||||
@@ -105,11 +105,11 @@ class LahanController extends Controller
|
|||||||
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'], // Max 5MB
|
'foto.*' => ['image', 'mimes:jpeg,png,jpg', 'max:5120'], // Max 5MB
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$targetProfileId = $user->role === 'fasilitator'
|
$targetProfileId = $user->role === 'enumerator'
|
||||||
? $validated['profile_id']
|
? $validated['profile_id']
|
||||||
: $profile->id;
|
: $profile->id;
|
||||||
|
|
||||||
if ($user->role === 'fasilitator') {
|
if ($user->role === 'enumerator') {
|
||||||
$targetProfile = Profile::findOrFail($targetProfileId);
|
$targetProfile = Profile::findOrFail($targetProfileId);
|
||||||
|
|
||||||
if ($targetProfile->desa_kelurahan_id !== $profile->desa_kelurahan_id) {
|
if ($targetProfile->desa_kelurahan_id !== $profile->desa_kelurahan_id) {
|
||||||
@@ -117,7 +117,7 @@ class LahanController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$desaKelurahanId = $user->role === 'fasilitator'
|
$desaKelurahanId = $user->role === 'enumerator'
|
||||||
? $profile->desa_kelurahan_id
|
? $profile->desa_kelurahan_id
|
||||||
: ($validated['desa_kelurahan_id'] ?? $profile->desa_kelurahan_id);
|
: ($validated['desa_kelurahan_id'] ?? $profile->desa_kelurahan_id);
|
||||||
|
|
||||||
@@ -149,14 +149,14 @@ class LahanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lahan = Lahan::with('desaKelurahan')->findOrFail($id);
|
$lahan = Lahan::with('desaKelurahan')->findOrFail($id);
|
||||||
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
||||||
return response()->json(['message' => 'Unauthorized'], 403);
|
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);
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +168,14 @@ class LahanController extends Controller
|
|||||||
*/
|
*/
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lahan = Lahan::findOrFail($id);
|
$lahan = Lahan::findOrFail($id);
|
||||||
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
||||||
return response()->json(['message' => 'Unauthorized'], 403);
|
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);
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,14 +215,14 @@ class LahanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lahan = Lahan::findOrFail($id);
|
$lahan = Lahan::findOrFail($id);
|
||||||
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
if ($lahan->profile_id !== Auth::user()->profile->id && Auth::user()->role === 'petani') {
|
||||||
return response()->json(['message' => 'Unauthorized'], 403);
|
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);
|
return response()->json(['message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ class LahanController extends Controller
|
|||||||
|
|
||||||
public function batchUpsert(Request $request): JsonResponse
|
public function batchUpsert(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class OfftakerController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class OfftakerController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class OfftakerController extends Controller
|
|||||||
|
|
||||||
public function show(string $id): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
$offtaker = Offtaker::findOrFail($id);
|
$offtaker = Offtaker::findOrFail($id);
|
||||||
@@ -67,7 +67,7 @@ class OfftakerController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $id): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
$offtaker = Offtaker::findOrFail($id);
|
$offtaker = Offtaker::findOrFail($id);
|
||||||
@@ -87,7 +87,7 @@ class OfftakerController extends Controller
|
|||||||
|
|
||||||
public function destroy(string $id): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
Offtaker::findOrFail($id)->delete();
|
Offtaker::findOrFail($id)->delete();
|
||||||
@@ -100,7 +100,7 @@ class OfftakerController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function batchUpsert(Request $request): JsonResponse
|
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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PanenController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class PanenController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator', 'super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class PanenController extends Controller
|
|||||||
|
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$panen = Panen::findOrFail($id);
|
$panen = Panen::findOrFail($id);
|
||||||
@@ -96,7 +96,7 @@ class PanenController extends Controller
|
|||||||
|
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
$panen = Panen::findOrFail($id);
|
$panen = Panen::findOrFail($id);
|
||||||
@@ -114,7 +114,7 @@ class PanenController extends Controller
|
|||||||
|
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
Gate::authorize(['petani', 'fasilitator']);
|
Gate::authorize(['petani', 'enumerator']);
|
||||||
Panen::findOrFail($id)->delete();
|
Panen::findOrFail($id)->delete();
|
||||||
return response()->json(null, 204);
|
return response()->json(null, 204);
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ class PanenController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function batchUpsert(Request $request): JsonResponse
|
public function batchUpsert(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (Gate::authorize(['petani', 'fasilitator','super_admin'])) {
|
if (Gate::authorize(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$profileId = Auth::user()->profile->id;
|
$profileId = Auth::user()->profile->id;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class PanenController01 extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class PanenController01 extends Controller
|
|||||||
|
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class PanenController01 extends Controller
|
|||||||
|
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$panen = Panen::findOrFail($id);
|
$panen = Panen::findOrFail($id);
|
||||||
@@ -67,7 +67,7 @@ class PanenController01 extends Controller
|
|||||||
|
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
$panen = Panen::findOrFail($id);
|
$panen = Panen::findOrFail($id);
|
||||||
@@ -85,7 +85,7 @@ class PanenController01 extends Controller
|
|||||||
|
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
Gate::authorize(['petani', 'fasilitator']);
|
Gate::authorize(['petani', 'enumerator']);
|
||||||
Panen::findOrFail($id)->delete();
|
Panen::findOrFail($id)->delete();
|
||||||
return response()->json(null, 204);
|
return response()->json(null, 204);
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ class PanenController01 extends Controller
|
|||||||
*/
|
*/
|
||||||
public function batchUpsert(Request $request): JsonResponse
|
public function batchUpsert(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (Gate::authorize(['petani', 'fasilitator','super_admin'])) {
|
if (Gate::authorize(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$profileId = Auth::user()->profile->id;
|
$profileId = Auth::user()->profile->id;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if(!Gate::any(['fasilitator', 'super_admin'])) {
|
if(!Gate::any(['enumerator', 'super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class ProfileController extends Controller
|
|||||||
$profileId = $user->role === 'petani' ? $profile->id : $requestID;
|
$profileId = $user->role === 'petani' ? $profile->id : $requestID;
|
||||||
|
|
||||||
$desaKelurahanId = null;
|
$desaKelurahanId = null;
|
||||||
if ($profile->role === 'fasilitator' || $profile->role === 'super_admin') {
|
if ($profile->role === 'enumerator' || $profile->role === 'super_admin') {
|
||||||
$desaKelurahanId = $profile->desa_kelurahan_id;
|
$desaKelurahanId = $profile->desa_kelurahan_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if(!Gate::any(['fasilitator','super_admin'])){
|
if(!Gate::any(['enumerator','super_admin'])){
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ class ProfileController extends Controller
|
|||||||
public function show(string $id): JSONResponse
|
public function show(string $id): JSONResponse
|
||||||
{
|
{
|
||||||
// attempt to access profile, only for allowed roles
|
// 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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class ProfileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function update(Request $request, string $id): JSONResponse
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$profile = Profile::findOrFail($id);
|
$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');
|
abort(403, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
public function downloadFile(string $id, string $type): JsonResponse | BinaryFileResponse
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
public function verify(Request $request, string $id): JSONResponse
|
public function verify(Request $request, string $id): JSONResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['fasilitator'])) {
|
if (!Gate::any(['enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): AnonymousResourceCollection
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
@@ -91,7 +91,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
$tanaman = Tanaman::findOrFail($id);
|
$tanaman = Tanaman::findOrFail($id);
|
||||||
@@ -195,7 +195,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$tanaman = Tanaman::findOrFail($id);
|
$tanaman = Tanaman::findOrFail($id);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request): AnonymousResourceCollection
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
@@ -91,7 +91,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
$tanaman = Tanaman::findOrFail($id);
|
$tanaman = Tanaman::findOrFail($id);
|
||||||
@@ -201,7 +201,7 @@ class TanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['petani', 'fasilitator','super_admin'])) {
|
if (!Gate::any(['petani', 'enumerator','super_admin'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$tanaman = Tanaman::findOrFail($id);
|
$tanaman = Tanaman::findOrFail($id);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PengurusBumdesController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class PengurusBumdesController extends Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth()->user()->role->value === 'fasilitator') {
|
if (auth()->user()->role->value === 'enumerator') {
|
||||||
$query->where('created_by', auth()->id());
|
$query->where('created_by', auth()->id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function edit(string $id)
|
public function edit(string $id)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $id)
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function destroy(string $id)
|
public function destroy(string $id)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function kelembagaanAktor(string $id)
|
public function kelembagaanAktor(string $id)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ class PengurusBumdesController extends Controller
|
|||||||
|
|
||||||
public function storeKelembagaanAktor(Request $request, string $id)
|
public function storeKelembagaanAktor(Request $request, string $id)
|
||||||
{
|
{
|
||||||
if (! Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (! Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ class PengurusBumdesController extends Controller
|
|||||||
{
|
{
|
||||||
$query = PengurusBumdes::query();
|
$query = PengurusBumdes::query();
|
||||||
|
|
||||||
if (auth()->user()->role->value === 'fasilitator') {
|
if (auth()->user()->role->value === 'enumerator') {
|
||||||
$query->where('created_by', auth()->id());
|
$query->where('created_by', auth()->id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,66 @@ use App\Models\Pertanian\Petani;
|
|||||||
use App\Models\Pertanian\Komoditas;
|
use App\Models\Pertanian\Komoditas;
|
||||||
use App\Models\Pertanian\Lahan;
|
use App\Models\Pertanian\Lahan;
|
||||||
use App\Models\Pertanian\PendapatanKomoditas;
|
use App\Models\Pertanian\PendapatanKomoditas;
|
||||||
|
use App\Models\Pertanian\SosialEkonomi;
|
||||||
|
|
||||||
class DemografiPetaniController extends Controller
|
class DemografiPetaniController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$petaniCounts = Petani::selectRaw("
|
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(*) as total,
|
||||||
COUNT(*) FILTER (WHERE jenis_kelamin = true) as laki,
|
COUNT(*) FILTER (WHERE jenis_kelamin = true) as laki,
|
||||||
COUNT(*) FILTER (WHERE jenis_kelamin = false) as perempuan
|
COUNT(*) FILTER (WHERE jenis_kelamin = false) as perempuan
|
||||||
@@ -30,14 +80,18 @@ class DemografiPetaniController extends Controller
|
|||||||
$petaniLakilaki = $petaniCounts->laki;
|
$petaniLakilaki = $petaniCounts->laki;
|
||||||
$petaniPerempuan = $petaniCounts->perempuan;
|
$petaniPerempuan = $petaniCounts->perempuan;
|
||||||
|
|
||||||
$totalKomoditas = Petani::whereNotNull('komoditas_id')
|
$totalKomoditas = Komoditas::query()
|
||||||
->distinct()
|
->where('is_utama', true)
|
||||||
->count('komoditas_id');
|
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
|
||||||
|
->count();
|
||||||
|
|
||||||
$totalLuasLahan = Lahan::has('petani')->sum('luas_lahan');
|
$totalLuasLahan = Lahan::has('petani')
|
||||||
|
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||||
|
->sum('luas_lahan');
|
||||||
$ratarataLuasLahan = $totalLuasLahan && $totalPetani ? $totalLuasLahan / $totalPetani : 0;
|
$ratarataLuasLahan = $totalLuasLahan && $totalPetani ? $totalLuasLahan / $totalPetani : 0;
|
||||||
|
|
||||||
$umurCounts = Petani::whereNotNull('tanggal_lahir')->selectRaw("
|
$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)) < 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 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 40 AND 49) as umur_40_49,
|
||||||
@@ -45,7 +99,8 @@ class DemografiPetaniController extends Controller
|
|||||||
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) >= 60) as umur_60_keatas
|
COUNT(*) FILTER (WHERE EXTRACT(YEAR FROM AGE(CURRENT_DATE, tanggal_lahir)) >= 60) as umur_60_keatas
|
||||||
")->first();
|
")->first();
|
||||||
|
|
||||||
$pendidikanCounts = Petani::whereNotNull('pendidikan_terakhir')
|
$pendidikanCounts = Petani::whereIn('petani.id', clone $filteredPetaniIds)
|
||||||
|
->whereNotNull('pendidikan_terakhir')
|
||||||
->select('pendidikan_terakhir', DB::raw('COUNT(*) as total'))
|
->select('pendidikan_terakhir', DB::raw('COUNT(*) as total'))
|
||||||
->groupBy('pendidikan_terakhir')
|
->groupBy('pendidikan_terakhir')
|
||||||
->pluck('total', 'pendidikan_terakhir');
|
->pluck('total', 'pendidikan_terakhir');
|
||||||
@@ -59,20 +114,71 @@ class DemografiPetaniController extends Controller
|
|||||||
's1_keatas' => (int) ($pendidikanCounts['06_sarjana'] ?? 0) + (int) ($pendidikanCounts['07_magister'] ?? 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()
|
$komoditasUtama = Komoditas::query()
|
||||||
->whereHas('petani')
|
->whereHas('petani', fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds))
|
||||||
->withCount('petani')
|
->withCount(['petani' => fn ($query) => $query->whereIn('petani.id', clone $filteredPetaniIds)])
|
||||||
->orderByDesc('petani_count')
|
->orderByDesc('petani_count')
|
||||||
->get(['id', 'nama']);
|
->get(['id', 'nama']);
|
||||||
|
|
||||||
$luasLahanBuckets = DB::query()->fromSub(
|
$luasLahanBuckets = DB::query()->fromSub(
|
||||||
Lahan::query()->select('petani_id')->selectRaw('SUM(luas_lahan) as total_luas')->groupBy('petani_id'),
|
Lahan::query()
|
||||||
|
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||||
|
->select('petani_id')
|
||||||
|
->selectRaw('luas_lahan'),
|
||||||
'agg'
|
'agg'
|
||||||
)->selectRaw("
|
)->selectRaw("
|
||||||
COUNT(*) FILTER (WHERE total_luas < 5000) as kurang_05,
|
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan < 0.5) as kurang_05,
|
||||||
COUNT(*) FILTER (WHERE total_luas >= 5000 AND total_luas <= 10000) as luas_05_1,
|
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan >= 0.5 AND luas_lahan <= 1) as luas_05_1,
|
||||||
COUNT(*) FILTER (WHERE total_luas > 10000 AND total_luas <= 20000) as luas_1_2,
|
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 1 AND luas_lahan <= 2) as luas_1_2,
|
||||||
COUNT(*) FILTER (WHERE total_luas > 20000) as lebih_2
|
COUNT(DISTINCT petani_id) FILTER (WHERE luas_lahan > 2) as lebih_2
|
||||||
")->first();
|
")->first();
|
||||||
|
|
||||||
$luasLahan = [
|
$luasLahan = [
|
||||||
@@ -82,14 +188,27 @@ class DemografiPetaniController extends Controller
|
|||||||
'lebih_2' => $luasLahanBuckets->lebih_2,
|
'lebih_2' => $luasLahanBuckets->lebih_2,
|
||||||
];
|
];
|
||||||
|
|
||||||
$penghasilanBuckets = DB::query()->fromSub(
|
$statusKepemilikanLahanRows = Lahan::query()
|
||||||
PendapatanKomoditas::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')
|
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
||||||
->whereNotNull('sosial_ekonomi.petani_id')
|
->whereNotNull('sosial_ekonomi.petani_id')
|
||||||
|
->whereIn('sosial_ekonomi.petani_id', clone $filteredPetaniIds)
|
||||||
->groupBy('sosial_ekonomi.petani_id')
|
->groupBy('sosial_ekonomi.petani_id')
|
||||||
->selectRaw('SUM(pendapatan_komoditas.pendapatan) as total_pendapatan'),
|
->selectRaw('SUM(pendapatan_komoditas.pendapatan) as total_pendapatan');
|
||||||
|
|
||||||
|
$penghasilanBuckets = DB::query()->fromSub(
|
||||||
|
clone $pendapatanPerPetaniQuery,
|
||||||
'agg'
|
'agg'
|
||||||
)->selectRaw("
|
)->selectRaw("
|
||||||
|
AVG(total_pendapatan) as rata_rata,
|
||||||
COUNT(*) FILTER (WHERE total_pendapatan < 1000000) as kurang_1,
|
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 >= 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 > 3000000 AND total_pendapatan <= 5000000) as p3_5,
|
||||||
@@ -97,6 +216,8 @@ class DemografiPetaniController extends Controller
|
|||||||
COUNT(*) FILTER (WHERE total_pendapatan > 10000000) as lebih_10
|
COUNT(*) FILTER (WHERE total_pendapatan > 10000000) as lebih_10
|
||||||
")->first();
|
")->first();
|
||||||
|
|
||||||
|
$ratarataPenghasilan = (float) ($penghasilanBuckets->rata_rata ?? 0);
|
||||||
|
|
||||||
$penghasilan = [
|
$penghasilan = [
|
||||||
'kurang_1' => $penghasilanBuckets->kurang_1,
|
'kurang_1' => $penghasilanBuckets->kurang_1,
|
||||||
'1_3' => $penghasilanBuckets->p1_3,
|
'1_3' => $penghasilanBuckets->p1_3,
|
||||||
@@ -112,6 +233,7 @@ class DemografiPetaniController extends Controller
|
|||||||
'total_komoditas' => $totalKomoditas,
|
'total_komoditas' => $totalKomoditas,
|
||||||
'total_luas_lahan' => $totalLuasLahan,
|
'total_luas_lahan' => $totalLuasLahan,
|
||||||
'ratarata_luas_lahan' => $ratarataLuasLahan,
|
'ratarata_luas_lahan' => $ratarataLuasLahan,
|
||||||
|
'ratarata_penghasilan' => $ratarataPenghasilan,
|
||||||
'kelompok_umur' => [
|
'kelompok_umur' => [
|
||||||
'kurang_30' => $umurCounts->kurang_30,
|
'kurang_30' => $umurCounts->kurang_30,
|
||||||
'30_39' => $umurCounts->umur_30_39,
|
'30_39' => $umurCounts->umur_30_39,
|
||||||
@@ -120,31 +242,88 @@ class DemografiPetaniController extends Controller
|
|||||||
'60_keatas' => $umurCounts->umur_60_keatas,
|
'60_keatas' => $umurCounts->umur_60_keatas,
|
||||||
],
|
],
|
||||||
'pendidikan' => $pendidikan,
|
'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' => [
|
'komoditas_utama' => [
|
||||||
'ids' => $komoditasUtama->pluck('id')->values(),
|
'ids' => $komoditasUtama->pluck('id')->values(),
|
||||||
'labels' => $komoditasUtama->pluck('nama')->values(),
|
'labels' => $komoditasUtama->pluck('nama')->values(),
|
||||||
'series' => $komoditasUtama->pluck('petani_count')->map(fn ($v) => (int) $v)->values(),
|
'series' => $komoditasUtama->pluck('petani_count')->map(fn ($v) => (int) $v)->values(),
|
||||||
],
|
],
|
||||||
'luas_lahan' => $luasLahan,
|
'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,
|
'penghasilan' => $penghasilan,
|
||||||
];
|
];
|
||||||
|
|
||||||
$daftarPetani = Petani::with(['komoditas'])
|
return compact('data');
|
||||||
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
}
|
||||||
->orderBy('id')
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('dashboard.demografi-petani', compact('data', 'daftarPetani'));
|
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)
|
public function data(Request $request)
|
||||||
{
|
{
|
||||||
|
$filteredPetaniIds = $this->filteredPetaniIdsQuery($request);
|
||||||
$pendapatanSub = PendapatanKomoditas::query()
|
$pendapatanSub = PendapatanKomoditas::query()
|
||||||
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
->join('sosial_ekonomi', 'sosial_ekonomi.id', '=', 'pendapatan_komoditas.sosial_ekonomi_id')
|
||||||
->whereColumn('sosial_ekonomi.petani_id', 'petani.id')
|
->whereColumn('sosial_ekonomi.petani_id', 'petani.id')
|
||||||
->selectRaw('SUM(pendapatan_komoditas.pendapatan)');
|
->selectRaw('SUM(pendapatan_komoditas.pendapatan)');
|
||||||
|
|
||||||
$query = Petani::with('komoditas')
|
$query = Petani::with('komoditas')
|
||||||
|
->whereIn('petani.id', clone $filteredPetaniIds)
|
||||||
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
||||||
->addSelect(['total_pendapatan' => $pendapatanSub]);
|
->addSelect(['total_pendapatan' => $pendapatanSub]);
|
||||||
|
|
||||||
@@ -187,6 +366,45 @@ class DemografiPetaniController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')) {
|
if ($request->filled('komoditas')) {
|
||||||
$query->where('komoditas_id', $request->komoditas);
|
$query->where('komoditas_id', $request->komoditas);
|
||||||
}
|
}
|
||||||
@@ -213,15 +431,28 @@ class DemografiPetaniController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('luas_lahan')) {
|
if ($request->filled('luas_lahan')) {
|
||||||
$lahanQuery = Lahan::query()->select('petani_id')->groupBy('petani_id');
|
$lahanQuery = Lahan::query()
|
||||||
|
->whereIn('petani_id', clone $filteredPetaniIds)
|
||||||
|
->select('petani_id')
|
||||||
|
->distinct();
|
||||||
|
|
||||||
match ($request->luas_lahan) {
|
match ($request->luas_lahan) {
|
||||||
'kurang_05' => $lahanQuery->havingRaw('SUM(luas_lahan) < 5000'),
|
'kurang_05' => $lahanQuery->where('luas_lahan', '<', 0.5),
|
||||||
'05_1' => $lahanQuery->havingRaw('SUM(luas_lahan) >= 5000 AND SUM(luas_lahan) <= 10000'),
|
'05_1' => $lahanQuery->whereBetween('luas_lahan', [0.5, 1]),
|
||||||
'1_2' => $lahanQuery->havingRaw('SUM(luas_lahan) > 10000 AND SUM(luas_lahan) <= 20000'),
|
'1_2' => $lahanQuery->where('luas_lahan', '>', 1)->where('luas_lahan', '<=', 2),
|
||||||
'lebih_2' => $lahanQuery->havingRaw('SUM(luas_lahan) > 20000'),
|
'lebih_2' => $lahanQuery->where('luas_lahan', '>', 2),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
$query->whereIn('id', $lahanQuery->pluck('petani_id'));
|
|
||||||
|
$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);
|
$daftarPetani = $query->orderBy('id')->paginate(10);
|
||||||
|
|||||||
@@ -5,21 +5,14 @@ namespace App\Http\Controllers\Web\Dashboard;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
use App\Models\Pertanian\Komoditas;
|
use App\Models\Pertanian\Komoditas;
|
||||||
use App\Models\Pertanian\Lahan;
|
use App\Models\Pertanian\Lahan;
|
||||||
use App\Models\Pertanian\Tanaman;
|
|
||||||
use App\Models\Pertanian\SkorPetani;
|
use App\Models\Pertanian\SkorPetani;
|
||||||
|
|
||||||
use App\Models\Wilayah\Provinsi;
|
|
||||||
use App\Models\Wilayah\KabupatenKota;
|
|
||||||
use App\Models\Wilayah\Kecamatan;
|
|
||||||
use App\Models\Wilayah\DesaKelurahan;
|
|
||||||
|
|
||||||
class IndeksPetaniController extends Controller
|
class IndeksPetaniController extends Controller
|
||||||
{
|
{
|
||||||
@@ -36,13 +29,13 @@ class IndeksPetaniController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function petaniQuery(array $filters): \Illuminate\Database\Eloquent\Builder
|
private function petaniQuery(array $filters): Builder
|
||||||
{
|
{
|
||||||
$petaniQuery = Petani::query()
|
$petaniQuery = Petani::query()
|
||||||
->join('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
||||||
->join('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
||||||
->join('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
||||||
->join('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
||||||
->whereNull('petani.deleted_at')
|
->whereNull('petani.deleted_at')
|
||||||
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
|
->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['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
|
||||||
@@ -50,17 +43,21 @@ class IndeksPetaniController extends Controller
|
|||||||
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_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));
|
->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;
|
return $petaniQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function skorPetaniQuery(array $filters, array $komoditas): \Illuminate\Database\Eloquent\Builder
|
private function skorPetaniQuery(array $filters, array $komoditas): Builder
|
||||||
{
|
{
|
||||||
$skor = SkorPetani::query()
|
$skor = SkorPetani::query()
|
||||||
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
->join('petani', 'petani_skor.petani_id', '=', 'petani.id')
|
||||||
->join('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
->leftJoin('master_desa_kelurahan', 'petani.desa_kelurahan_id', '=', 'master_desa_kelurahan.id')
|
||||||
->join('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
->leftJoin('master_kecamatan', 'master_desa_kelurahan.kecamatan_id', '=', 'master_kecamatan.id')
|
||||||
->join('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
->leftJoin('master_kabupaten_kota', 'master_kecamatan.kabupaten_kota_id', '=', 'master_kabupaten_kota.id')
|
||||||
->join('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
->leftJoin('master_provinsi', 'master_kabupaten_kota.provinsi_id', '=', 'master_provinsi.id')
|
||||||
->whereNull('petani.deleted_at')
|
->whereNull('petani.deleted_at')
|
||||||
->when($filters['provinsi_id'], fn($q, $id) => $q->where('master_kabupaten_kota.provinsi_id', $id))
|
->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['kabupatenKota_id'], fn($q, $id) => $q->where('master_kecamatan.kabupaten_kota_id', $id))
|
||||||
@@ -68,53 +65,61 @@ class IndeksPetaniController extends Controller
|
|||||||
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
|
->when($filters['desaKelurahan_id'], fn($q, $id) => $q->where('petani.desa_kelurahan_id', $id))
|
||||||
->whereIn('petani.komoditas_id', $komoditas);
|
->whereIn('petani.komoditas_id', $komoditas);
|
||||||
|
|
||||||
if ($filters['segmen']) {
|
return $this->applySegmentFilter($skor, $filters['segmen']);
|
||||||
$skor->where(function ($q) use ($filters) {
|
|
||||||
match ($filters['segmen']) {
|
|
||||||
'Penggerak' => $q->where('skor_kesiapan_adopsi', '>=', 4),
|
|
||||||
'Perintis' => $q->where(function($q) {
|
|
||||||
$q->where('skor_kesiapan_adopsi', '>=', 3)
|
|
||||||
->where('skor_kesiapan_adopsi', '<', 4);
|
|
||||||
}),
|
|
||||||
'Pemerhati' => $q->where(function($q) {
|
|
||||||
$q->where('skor_kesiapan_adopsi', '>=', 2)
|
|
||||||
->where('skor_kesiapan_adopsi', '<', 3);
|
|
||||||
}),
|
|
||||||
'Pemula' => $q->where(function($q) {
|
|
||||||
$q->where('skor_kesiapan_adopsi', '>=', 1)
|
|
||||||
->where('skor_kesiapan_adopsi', '<', 2);
|
|
||||||
}),
|
|
||||||
'Belum Lengkap' => $q->where(function ($q) {
|
|
||||||
$q->whereNull('skor_kesiapan_adopsi')
|
|
||||||
->orWhere('skor_kesiapan_adopsi', '<', 1);
|
|
||||||
}),
|
|
||||||
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $skor;
|
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)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
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);
|
$filters = $this->filters($request);
|
||||||
|
|
||||||
$title = 'Dashboard Profil Petani';
|
$segments = $this->segments();
|
||||||
|
$colors = collect($segments)
|
||||||
|
->pluck('color')
|
||||||
|
->map(fn($c) => "var(--bs-{$c})")
|
||||||
|
->values();
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)->get();
|
$komoditas = Komoditas::where('is_utama', true)->get(['id', 'nama']);
|
||||||
$petaniQuery = $this->petaniQuery($filters);
|
$petaniQuery = $this->petaniQuery(array_fill_keys(array_keys($filters), null));
|
||||||
|
|
||||||
$desaKelurahan = $petaniQuery->clone()
|
$desaKelurahan = $petaniQuery->clone()
|
||||||
->selectRaw("
|
->selectRaw("
|
||||||
master_desa_kelurahan.id as desa_kelurahan_id,
|
master_desa_kelurahan.id as desa_kelurahan_id,
|
||||||
master_desa_kelurahan.nama as desa_kelurahan,
|
master_desa_kelurahan.nama as desa_kelurahan,
|
||||||
master_desa_kelurahan.path as path,
|
|
||||||
master_kecamatan.id as kecamatan_id,
|
master_kecamatan.id as kecamatan_id,
|
||||||
master_kecamatan.nama as kecamatan,
|
master_kecamatan.nama as kecamatan,
|
||||||
master_kabupaten_kota.id as kabupaten_id,
|
master_kabupaten_kota.id as kabupaten_id,
|
||||||
@@ -122,9 +127,15 @@ class IndeksPetaniController extends Controller
|
|||||||
master_provinsi.id as provinsi_id,
|
master_provinsi.id as provinsi_id,
|
||||||
master_provinsi.nama as provinsi
|
master_provinsi.nama as provinsi
|
||||||
")
|
")
|
||||||
|
->whereNotNull('master_provinsi.id')
|
||||||
->distinct()
|
->distinct()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$provinsi = $desaKelurahan
|
||||||
|
->unique('provinsi_id')
|
||||||
|
->sortBy('provinsi')
|
||||||
|
->values();
|
||||||
|
|
||||||
$wilayah = $desaKelurahan
|
$wilayah = $desaKelurahan
|
||||||
->groupBy('provinsi_id')
|
->groupBy('provinsi_id')
|
||||||
->map(function ($provinsi) {
|
->map(function ($provinsi) {
|
||||||
@@ -147,7 +158,6 @@ class IndeksPetaniController extends Controller
|
|||||||
return [
|
return [
|
||||||
'id' => $desa->desa_kelurahan_id,
|
'id' => $desa->desa_kelurahan_id,
|
||||||
'nama' => $desa->desa_kelurahan,
|
'nama' => $desa->desa_kelurahan,
|
||||||
'path' => $desa->path,
|
|
||||||
];
|
];
|
||||||
})->values(),
|
})->values(),
|
||||||
];
|
];
|
||||||
@@ -157,12 +167,15 @@ class IndeksPetaniController extends Controller
|
|||||||
];
|
];
|
||||||
})->values();
|
})->values();
|
||||||
|
|
||||||
return view('dashboard.indeks-petani', array_merge($filters,
|
return array_merge($filters,
|
||||||
compact(
|
compact(
|
||||||
'komoditas',
|
'komoditas',
|
||||||
'desaKelurahan',
|
'desaKelurahan',
|
||||||
|
'provinsi',
|
||||||
'wilayah',
|
'wilayah',
|
||||||
)));
|
'segments',
|
||||||
|
'colors',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function summary(Request $request): JsonResponse
|
public function summary(Request $request): JsonResponse
|
||||||
@@ -171,23 +184,21 @@ class IndeksPetaniController extends Controller
|
|||||||
$petaniQuery = $this->petaniQuery($filters);
|
$petaniQuery = $this->petaniQuery($filters);
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$countKomoditas = $petaniQuery->clone()
|
$counts = $petaniQuery->clone()
|
||||||
->whereHas('komoditas', fn ($query) => $query->where('is_utama', true))
|
->selectRaw('COUNT(*) as petani')
|
||||||
->distinct()->count('komoditas_id');
|
->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
|
$filteredPetaniIds = $petaniQuery->clone()->select('petani.id');
|
||||||
->clone()
|
$lahan = Lahan::query()
|
||||||
->select('petani.id')
|
->whereIn('petani_id', $filteredPetaniIds)
|
||||||
->distinct()
|
->whereIn('komoditas_id', $komoditas->pluck('id'))
|
||||||
->pluck('petani.id');
|
->selectRaw('COALESCE(SUM(luas_lahan), 0) as total, AVG(luas_lahan) as rata_rata')
|
||||||
|
->first();
|
||||||
$sumLahan = Lahan::has('petani')->whereIn('petani_id', $filteredPetaniIds)->whereIn('komoditas_id', $komoditas->pluck('id'))->sum('luas_lahan');
|
|
||||||
$avgLahan = Lahan::has('petani')->whereIn('petani_id', $filteredPetaniIds)->whereIn('komoditas_id', $komoditas->pluck('id'))->avg('luas_lahan');
|
|
||||||
|
|
||||||
$countDesa = $petaniQuery->clone()->whereIn('komoditas_id', $komoditas->pluck('id'))->distinct()->count('desa_kelurahan_id');
|
|
||||||
$countKecamatan = $petaniQuery->clone()->whereIn('komoditas_id', $komoditas->pluck('id'))->distinct()->count('master_kecamatan.id');
|
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
$avgSkor = $skorQuery->clone()->avg('skor_kesiapan_adopsi');
|
$avgSkor = $skorQuery->clone()->avg('skor_kesiapan_adopsi');
|
||||||
@@ -205,12 +216,12 @@ class IndeksPetaniController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'petani' => $petaniQuery->clone()->count(),
|
'petani' => (int) $counts->petani,
|
||||||
'komoditas' => $countKomoditas,
|
'komoditas' => (int) $counts->komoditas,
|
||||||
'sum_lahan' => $sumLahan,
|
'sum_lahan' => (float) $lahan->total,
|
||||||
'avg_lahan' => $avgLahan,
|
'avg_lahan' => (float) $lahan->rata_rata,
|
||||||
'desa' => $countDesa,
|
'desa' => (int) $counts->desa,
|
||||||
'kecamatan' => $countKecamatan,
|
'kecamatan' => (int) $counts->kecamatan,
|
||||||
'skor' => $avgSkor,
|
'skor' => $avgSkor,
|
||||||
'segmen' => $avgSegmen
|
'segmen' => $avgSegmen
|
||||||
]);
|
]);
|
||||||
@@ -269,7 +280,7 @@ class IndeksPetaniController extends Controller
|
|||||||
$case = $this->cases();
|
$case = $this->cases();
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
|
|
||||||
@@ -313,7 +324,7 @@ class IndeksPetaniController extends Controller
|
|||||||
$case = $this->cases();
|
$case = $this->cases();
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
|
|
||||||
@@ -355,7 +366,7 @@ class IndeksPetaniController extends Controller
|
|||||||
$filters = $this->filters($request);
|
$filters = $this->filters($request);
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
|
|
||||||
@@ -380,7 +391,7 @@ class IndeksPetaniController extends Controller
|
|||||||
$filters = $this->filters($request);
|
$filters = $this->filters($request);
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
|
|
||||||
@@ -405,17 +416,8 @@ class IndeksPetaniController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function map(Request $request): JsonResponse
|
private function villageData(array $filters, bool $includePath = true): \Illuminate\Support\Collection
|
||||||
{
|
{
|
||||||
$filters = $this->filters($request);
|
|
||||||
$segments = $this->segments();
|
|
||||||
|
|
||||||
$colors = collect($segments)
|
|
||||||
->pluck('color')
|
|
||||||
->map(fn($c) => "var(--bs-{$c})")
|
|
||||||
->values();
|
|
||||||
$segmentStyles = collect($segments)->keyBy('name');
|
|
||||||
|
|
||||||
$case = $this->cases();
|
$case = $this->cases();
|
||||||
|
|
||||||
$petaniQuery = $this->petaniQuery($filters);
|
$petaniQuery = $this->petaniQuery($filters);
|
||||||
@@ -424,7 +426,6 @@ class IndeksPetaniController extends Controller
|
|||||||
->selectRaw("
|
->selectRaw("
|
||||||
master_desa_kelurahan.id as desa_kelurahan_id,
|
master_desa_kelurahan.id as desa_kelurahan_id,
|
||||||
master_desa_kelurahan.nama as desa_kelurahan,
|
master_desa_kelurahan.nama as desa_kelurahan,
|
||||||
master_desa_kelurahan.path as path,
|
|
||||||
master_kecamatan.id as kecamatan_id,
|
master_kecamatan.id as kecamatan_id,
|
||||||
master_kecamatan.nama as kecamatan,
|
master_kecamatan.nama as kecamatan,
|
||||||
master_kabupaten_kota.id as kabupaten_id,
|
master_kabupaten_kota.id as kabupaten_id,
|
||||||
@@ -432,11 +433,12 @@ class IndeksPetaniController extends Controller
|
|||||||
master_provinsi.id as provinsi_id,
|
master_provinsi.id as provinsi_id,
|
||||||
master_provinsi.nama as provinsi
|
master_provinsi.nama as provinsi
|
||||||
")
|
")
|
||||||
|
->when($includePath, fn (Builder $query) => $query->addSelect('master_desa_kelurahan.path as path'))
|
||||||
->distinct()
|
->distinct()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
$skorQuery = $this->skorPetaniQuery($filters, $komoditas->pluck('id')->toArray());
|
||||||
|
|
||||||
@@ -464,17 +466,21 @@ class IndeksPetaniController extends Controller
|
|||||||
->groupBy('petani.desa_kelurahan_id')
|
->groupBy('petani.desa_kelurahan_id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$segmenDominan = $mapSegmentasi
|
$segmentasiGroups = $mapSegmentasi->groupBy('desa_kelurahan_id');
|
||||||
->groupBy('desa_kelurahan_id')
|
$jumlahSegmenByDesa = $segmentasiGroups
|
||||||
|
->map(fn ($items) => $items->pluck('total', 'segmen'));
|
||||||
|
|
||||||
|
$segmenDominan = $segmentasiGroups
|
||||||
->map(function ($items) {
|
->map(function ($items) {
|
||||||
return $items->sortByDesc('total')->first();
|
return $items->reduce(fn ($largest, $item) => $largest === null || $item->total > $largest->total ? $item : $largest);
|
||||||
});
|
});
|
||||||
|
|
||||||
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan) {
|
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan, $jumlahSegmenByDesa) {
|
||||||
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
|
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
|
||||||
|
|
||||||
$item->segmen_dominan = $dominan?->segmen;
|
$item->segmen_dominan = $dominan?->segmen;
|
||||||
$item->jumlah_dominan = $dominan?->total;
|
$item->jumlah_dominan = $dominan?->total;
|
||||||
|
$item->jumlah_segmen = $jumlahSegmenByDesa->get($item->desa_kelurahan_id, collect());
|
||||||
});
|
});
|
||||||
|
|
||||||
$segmentasiByDesa = $mapAvgSkorSegmentasi->keyBy('desa_kelurahan_id');
|
$segmentasiByDesa = $mapAvgSkorSegmentasi->keyBy('desa_kelurahan_id');
|
||||||
@@ -490,97 +496,22 @@ class IndeksPetaniController extends Controller
|
|||||||
return $desa;
|
return $desa;
|
||||||
})->filter()->values();
|
})->filter()->values();
|
||||||
|
|
||||||
return response()->json($mapDesaSegmentasi);
|
return $mapDesaSegmentasi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function map(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json($this->villageData($this->filters($request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function desa(Request $request): JsonResponse
|
public function desa(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$filters = $this->filters($request);
|
$segmentStyles = collect($this->segments())->keyBy('name');
|
||||||
$segments = $this->segments();
|
$mapDesaSegmentasi = $this->villageData($this->filters($request), false);
|
||||||
|
|
||||||
$colors = collect($segments)
|
$segmentNames = ['Pemula', 'Perintis', 'Pemerhati', 'Penggerak', 'Belum Lengkap'];
|
||||||
->pluck('color')
|
$rows = $mapDesaSegmentasi->map(function ($desa) use ($segmentStyles, $segmentNames) {
|
||||||
->map(fn($c) => "var(--bs-{$c})")
|
$total = (int) $desa->segmentasi->jumlah_petani;
|
||||||
->values();
|
|
||||||
$segmentStyles = collect($segments)->keyBy('name');
|
|
||||||
|
|
||||||
$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_desa_kelurahan.path as path,
|
|
||||||
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
|
|
||||||
")
|
|
||||||
->distinct()
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
$segmenDominan = $mapSegmentasi
|
|
||||||
->groupBy('desa_kelurahan_id')
|
|
||||||
->map(function ($items) {
|
|
||||||
return $items->sortByDesc('total')->first();
|
|
||||||
});
|
|
||||||
|
|
||||||
$mapAvgSkorSegmentasi->each(function ($item) use ($segmenDominan) {
|
|
||||||
$dominan = $segmenDominan->get($item->desa_kelurahan_id);
|
|
||||||
|
|
||||||
$item->segmen_dominan = $dominan?->segmen;
|
|
||||||
$item->jumlah_dominan = $dominan?->total;
|
|
||||||
});
|
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
$rows = $mapDesaSegmentasi->map(function ($desa) use ($segmentStyles) {
|
|
||||||
|
|
||||||
$style = $segmentStyles[$desa->segmentasi->segmen] ?? ['color' => 'secondary'];
|
$style = $segmentStyles[$desa->segmentasi->segmen] ?? ['color' => 'secondary'];
|
||||||
$styleDominan = $segmentStyles[$desa->segmentasi->segmen_dominan] ?? ['color' => 'secondary'];
|
$styleDominan = $segmentStyles[$desa->segmentasi->segmen_dominan] ?? ['color' => 'secondary'];
|
||||||
@@ -594,6 +525,16 @@ class IndeksPetaniController extends Controller
|
|||||||
'jumlah_dominan' => $desa->segmentasi->jumlah_dominan,
|
'jumlah_dominan' => $desa->segmentasi->jumlah_dominan,
|
||||||
'color' => $style['color'],
|
'color' => $style['color'],
|
||||||
'color_dominan' => $styleDominan['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(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,30 +546,21 @@ class IndeksPetaniController extends Controller
|
|||||||
$filters = $this->filters($request);
|
$filters = $this->filters($request);
|
||||||
$segments = $this->segments();
|
$segments = $this->segments();
|
||||||
|
|
||||||
$colors = collect($segments)
|
|
||||||
->pluck('color')
|
|
||||||
->map(fn($c) => "var(--bs-{$c})")
|
|
||||||
->values();
|
|
||||||
$segmentStyles = collect($segments)->keyBy('name');
|
$segmentStyles = collect($segments)->keyBy('name');
|
||||||
|
|
||||||
$petaniQuery = $this->petaniQuery($filters);
|
$petaniQuery = $this->petaniQuery($filters);
|
||||||
|
|
||||||
$komoditas = Komoditas::where('is_utama', true)
|
$komoditas = Komoditas::where('is_utama', true)
|
||||||
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get();
|
->when($filters['komoditas_id'], fn($q, $id) => $q->where('id', $id))->get(['id', 'nama']);
|
||||||
|
|
||||||
$daftarPetani = $petaniQuery->clone()
|
$daftarPetani = $petaniQuery->clone()
|
||||||
->select('petani.*')
|
->select('petani.*')
|
||||||
|
->with(['desaKelurahan', 'komoditas', 'skor'])
|
||||||
|
->withSum('lahans as total_luas_lahan', 'luas_lahan')
|
||||||
->whereIn('komoditas_id', $komoditas->pluck('id'))
|
->whereIn('komoditas_id', $komoditas->pluck('id'))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$data = $daftarPetani
|
$data = $daftarPetani
|
||||||
->filter(function ($item) use ($filters) {
|
|
||||||
if (!$filters['segmen']) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$segmen = $item->skor ? $item->segmen() : 'Belum Lengkap';
|
|
||||||
return $segmen === $filters['segmen'];
|
|
||||||
})
|
|
||||||
->map(function ($item) use ($segmentStyles) {
|
->map(function ($item) use ($segmentStyles) {
|
||||||
$segmen = $item->skor
|
$segmen = $item->skor
|
||||||
? $item->segmen()
|
? $item->segmen()
|
||||||
@@ -642,7 +574,7 @@ class IndeksPetaniController extends Controller
|
|||||||
'desa' => $item->desaKelurahan?->nama,
|
'desa' => $item->desaKelurahan?->nama,
|
||||||
'desa_kelurahan_id' => $item->desa_kelurahan_id,
|
'desa_kelurahan_id' => $item->desa_kelurahan_id,
|
||||||
'komoditas' => $item->komoditas?->nama,
|
'komoditas' => $item->komoditas?->nama,
|
||||||
'luas_lahan' => $item->lahans?->sum('luas_lahan') ?? 0,
|
'luas_lahan' => (float) ($item->total_luas_lahan ?? 0),
|
||||||
'skor' => $item->skor
|
'skor' => $item->skor
|
||||||
? $item->skor->skor_kesiapan_adopsi
|
? $item->skor->skor_kesiapan_adopsi
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Web\Dashboard;
|
|||||||
|
|
||||||
use App\Enums\KeparahanSerangan;
|
use App\Enums\KeparahanSerangan;
|
||||||
use App\Enums\PenyakitTanaman;
|
use App\Enums\PenyakitTanaman;
|
||||||
|
use App\Enums\PraktikBudidaya\KondisiPengelolaanAir;
|
||||||
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
use App\Enums\PraktikBudidaya\PemberianPupuk;
|
||||||
use App\Enums\SeranganHama;
|
use App\Enums\SeranganHama;
|
||||||
use App\Enums\SosialEkonomi\StatusKepemilikanLahanSosialEkonomi;
|
use App\Enums\SosialEkonomi\StatusKepemilikanLahanSosialEkonomi;
|
||||||
@@ -13,12 +14,14 @@ use App\Models\Pertanian\Lahan;
|
|||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
use App\Models\Pertanian\PraktikBudidaya;
|
use App\Models\Pertanian\PraktikBudidaya;
|
||||||
use App\Models\Pertanian\Produksi;
|
use App\Models\Pertanian\Produksi;
|
||||||
|
use App\Models\Pertanian\SurveyPanen;
|
||||||
use App\Models\Pertanian\SkorPetani;
|
use App\Models\Pertanian\SkorPetani;
|
||||||
use App\Models\Pertanian\SosialEkonomi;
|
use App\Models\Pertanian\SosialEkonomi;
|
||||||
use App\Models\Pertanian\SurveyProduktivitas;
|
use App\Models\Pertanian\SurveyProduktivitas;
|
||||||
use App\Models\Wilayah\DesaKelurahan;
|
use App\Models\Wilayah\DesaKelurahan;
|
||||||
use App\Models\Wilayah\KabupatenKota;
|
use App\Models\Wilayah\KabupatenKota;
|
||||||
use App\Models\Wilayah\Kecamatan;
|
use App\Models\Wilayah\Kecamatan;
|
||||||
|
use App\Models\Wilayah\Provinsi;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
@@ -95,11 +98,180 @@ class IndikatorPertanianController extends Controller
|
|||||||
private function dashboardPayload(array $filters): array
|
private function dashboardPayload(array $filters): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'overviewData' => $this->overviewData($filters),
|
||||||
|
'landProfileData' => $this->landProfileData($filters),
|
||||||
'summaryData' => $this->summaryData($filters),
|
'summaryData' => $this->summaryData($filters),
|
||||||
'chartData' => $this->chartData($filters),
|
'chartData' => $this->chartData($filters),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function overviewData(array $filters): array
|
||||||
|
{
|
||||||
|
$totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count();
|
||||||
|
$totalLahan = $this->applyLahanFilters(Lahan::query(), $filters)->count();
|
||||||
|
$totalLuasLahan = (float) $this->applyLahanFilters(Lahan::has('petani'), $filters)->sum('luas_lahan');
|
||||||
|
$jumlahDesa = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('desa_kelurahan_id')
|
||||||
|
->distinct('desa_kelurahan_id')
|
||||||
|
->count('desa_kelurahan_id');
|
||||||
|
$jumlahKecamatan = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->leftJoin('master_desa_kelurahan as d', 'd.id', '=', 'map_lahan.desa_kelurahan_id')
|
||||||
|
->whereNotNull('d.kecamatan_id')
|
||||||
|
->distinct('d.kecamatan_id')
|
||||||
|
->count('d.kecamatan_id');
|
||||||
|
$totalProduksiKg = $this->totalProduksiKg($filters);
|
||||||
|
$rataRataProduktivitas = $totalLuasLahan > 0 ? $totalProduksiKg / $totalLuasLahan : 0;
|
||||||
|
$tanamanProduktif = $this->tanamanProduktifCount($filters);
|
||||||
|
$batchPanen = $this->batchPanenCount($filters);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'kpiCards' => [
|
||||||
|
[
|
||||||
|
'value' => number_format($totalPetani, 0, ',', '.'),
|
||||||
|
'label' => 'Jumlah Petani',
|
||||||
|
'description' => 'Petani yang tercakup dalam filter aktif',
|
||||||
|
'icon' => 'tabler-users',
|
||||||
|
'variant' => 'primary',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($totalLahan, 0, ',', '.'),
|
||||||
|
'label' => 'Jumlah Kebun/Lahan',
|
||||||
|
'description' => 'Kebun atau lahan terdata',
|
||||||
|
'icon' => 'tabler-trees',
|
||||||
|
'variant' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($totalLuasLahan, 2, ',', '.').' ha',
|
||||||
|
'label' => 'Luas Lahan',
|
||||||
|
'description' => 'Akumulasi luas lahan terdata',
|
||||||
|
'icon' => 'tabler-ruler-measure',
|
||||||
|
'variant' => 'warning',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($jumlahDesa, 0, ',', '.').' desa',
|
||||||
|
'label' => 'Desa/Kecamatan',
|
||||||
|
'description' => number_format($jumlahKecamatan, 0, ',', '.').' kecamatan tercakup',
|
||||||
|
'icon' => 'tabler-map-pin',
|
||||||
|
'variant' => 'info',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($totalProduksiKg, 0, ',', '.').' kg',
|
||||||
|
'label' => 'Total Produksi',
|
||||||
|
'description' => $this->productionSummaryDescription($filters),
|
||||||
|
'icon' => 'tabler-wheat',
|
||||||
|
'variant' => 'primary',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($rataRataProduktivitas, 0, ',', '.').' kg/ha',
|
||||||
|
'label' => 'Produktivitas Rata-rata',
|
||||||
|
'description' => 'Berdasarkan produksi dan luas lahan terdata',
|
||||||
|
'icon' => 'tabler-chart-bar',
|
||||||
|
'variant' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($tanamanProduktif, 0, ',', '.'),
|
||||||
|
'label' => 'Tanaman Produktif',
|
||||||
|
'description' => 'Tanaman dengan kondisi baik',
|
||||||
|
'icon' => 'tabler-plant-2',
|
||||||
|
'variant' => 'warning',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($batchPanen, 0, ',', '.'),
|
||||||
|
'label' => 'Batch Panen Tercatat',
|
||||||
|
'description' => 'Catatan produksi dan survey panen',
|
||||||
|
'icon' => 'tabler-basket',
|
||||||
|
'variant' => 'info',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'commodityFocus' => $this->commodityFocusData($filters),
|
||||||
|
'map' => [
|
||||||
|
'features' => $this->farmMapFeatures($filters),
|
||||||
|
'levels' => $this->mapLevelData($filters),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function landProfileData(array $filters): array
|
||||||
|
{
|
||||||
|
$totalTanaman = (int) $this->applyLahanFilters(Lahan::query(), $filters)->sum('jumlah_tanaman');
|
||||||
|
$rataRataUmurTanaman = (float) $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('umur_tanaman')
|
||||||
|
->avg('umur_tanaman');
|
||||||
|
$komoditasCount = (int) $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('komoditas_id')
|
||||||
|
->distinct('komoditas_id')
|
||||||
|
->count('komoditas_id');
|
||||||
|
$statusLahanCount = (int) $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('status_kepemilikan')
|
||||||
|
->distinct('status_kepemilikan')
|
||||||
|
->count('status_kepemilikan');
|
||||||
|
$tanamanPenaung = $this->shadowTreeSummary($filters);
|
||||||
|
$tanamanPendamping = $this->companionPlantSummary($filters);
|
||||||
|
$diversityAverage = $this->plantDiversityAverage($filters);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'conditionCards' => [
|
||||||
|
[
|
||||||
|
'value' => number_format($komoditasCount, 0, ',', '.'),
|
||||||
|
'label' => 'Luas Lahan per Komoditas',
|
||||||
|
'description' => 'Komoditas dengan luas lahan terdata',
|
||||||
|
'icon' => 'tabler-chart-treemap',
|
||||||
|
'variant' => 'primary',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($totalTanaman, 0, ',', '.'),
|
||||||
|
'label' => 'Jumlah Tanaman',
|
||||||
|
'description' => 'Akumulasi jumlah tanaman pada lahan',
|
||||||
|
'icon' => 'tabler-trees',
|
||||||
|
'variant' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($rataRataUmurTanaman, 1, ',', '.').' tahun',
|
||||||
|
'label' => 'Umur Tanaman',
|
||||||
|
'description' => 'Rata-rata umur tanaman terdata',
|
||||||
|
'icon' => 'tabler-calendar-stats',
|
||||||
|
'variant' => 'warning',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($statusLahanCount, 0, ',', '.'),
|
||||||
|
'label' => 'Status Lahan',
|
||||||
|
'description' => 'Jumlah kategori status kepemilikan lahan',
|
||||||
|
'icon' => 'tabler-certificate',
|
||||||
|
'variant' => 'info',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'agroecologyItems' => [
|
||||||
|
['label' => 'Sumber Air Lahan', 'value' => $this->sourceWaterSummary($filters), 'status' => 'available'],
|
||||||
|
['label' => 'Pengelolaan Air & Irigasi', 'value' => $this->waterManagementSummary($filters), 'status' => 'available'],
|
||||||
|
['label' => 'Kebutuhan Air per Musim', 'value' => $this->seasonalWaterNeedSummary($filters), 'status' => 'available'],
|
||||||
|
['label' => 'Praktik Konservasi Tanah', 'value' => $this->soilConservationSummary($filters), 'status' => 'available'],
|
||||||
|
],
|
||||||
|
'agroforestryCards' => [
|
||||||
|
[
|
||||||
|
'value' => number_format($tanamanPenaung['total'], 0, ',', '.'),
|
||||||
|
'label' => 'Jumlah Tanaman Penaung',
|
||||||
|
'description' => $tanamanPenaung['types'].' jenis tanaman penaung',
|
||||||
|
'icon' => 'tabler-leaf',
|
||||||
|
'variant' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format($tanamanPendamping['types'], 0, ',', '.'),
|
||||||
|
'label' => 'Jenis Tanaman Pendamping',
|
||||||
|
'description' => number_format($tanamanPendamping['total'], 0, ',', '.').' entri tanaman pendamping',
|
||||||
|
'icon' => 'tabler-flower',
|
||||||
|
'variant' => 'warning',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => number_format(round($diversityAverage), 0, ',', '.'),
|
||||||
|
'label' => 'Tingkat Campuran Tanaman',
|
||||||
|
'description' => 'Rata-rata kombinasi tanaman utama, pendamping, dan penaung',
|
||||||
|
'icon' => 'tabler-binary-tree',
|
||||||
|
'variant' => 'primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function summaryData(array $filters): array
|
private function summaryData(array $filters): array
|
||||||
{
|
{
|
||||||
$totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count();
|
$totalPetani = $this->applyPetaniFilters(Petani::query(), $filters)->count();
|
||||||
@@ -166,6 +338,10 @@ class IndikatorPertanianController extends Controller
|
|||||||
'statusPanen' => $this->statusPanenChart($filters),
|
'statusPanen' => $this->statusPanenChart($filters),
|
||||||
'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($filters, 'ha_tahun'),
|
'surveyProduktivitasHa' => $this->surveyProduktivitasByUnitChart($filters, 'ha_tahun'),
|
||||||
'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($filters, 'pohon_tahun'),
|
'surveyProduktivitasPohon' => $this->surveyProduktivitasByUnitChart($filters, 'pohon_tahun'),
|
||||||
|
'luasLahanPerKomoditas' => $this->luasLahanPerKomoditasChart($filters),
|
||||||
|
'jumlahTanamanPerKomoditas' => $this->jumlahTanamanPerKomoditasChart($filters),
|
||||||
|
'umurTanaman' => $this->umurTanamanChart($filters),
|
||||||
|
'sumberAir' => $this->sumberAirChart($filters),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +531,82 @@ class IndikatorPertanianController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function luasLahanPerKomoditasChart(array $filters): array
|
||||||
|
{
|
||||||
|
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
|
||||||
|
->whereNotNull('map_lahan.luas_lahan')
|
||||||
|
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||||
|
->selectRaw('SUM(map_lahan.luas_lahan) as total')
|
||||||
|
->groupBy('k.id', 'k.nama')
|
||||||
|
->havingRaw('SUM(map_lahan.luas_lahan) > 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 jumlahTanamanPerKomoditasChart(array $filters): array
|
||||||
|
{
|
||||||
|
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
|
||||||
|
->whereNotNull('map_lahan.jumlah_tanaman')
|
||||||
|
->selectRaw("COALESCE(k.nama, 'Belum ditentukan') as label")
|
||||||
|
->selectRaw('SUM(map_lahan.jumlah_tanaman) as total')
|
||||||
|
->groupBy('k.id', 'k.nama')
|
||||||
|
->havingRaw('SUM(map_lahan.jumlah_tanaman) > 0')
|
||||||
|
->orderByDesc('total')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'labels' => $rows->pluck('label')->values()->all(),
|
||||||
|
'series' => $rows->pluck('total')->map(fn ($value) => (int) $value)->values()->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function umurTanamanChart(array $filters): array
|
||||||
|
{
|
||||||
|
$bucketOrder = ['< 3 tahun', '3-7 tahun', '8-15 tahun', '> 15 tahun'];
|
||||||
|
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('umur_tanaman')
|
||||||
|
->pluck('umur_tanaman')
|
||||||
|
->map(function ($age) {
|
||||||
|
$age = (float) $age;
|
||||||
|
|
||||||
|
return match (true) {
|
||||||
|
$age < 3 => '< 3 tahun',
|
||||||
|
$age <= 7 => '3-7 tahun',
|
||||||
|
$age <= 15 => '8-15 tahun',
|
||||||
|
default => '> 15 tahun',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
->countBy()
|
||||||
|
->sortBy(fn ($value, $label) => array_search($label, $bucketOrder, true));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'labels' => $rows->keys()->values()->all(),
|
||||||
|
'series' => $rows->values()->map(fn ($value) => (int) $value)->values()->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sumberAirChart(array $filters): array
|
||||||
|
{
|
||||||
|
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||||
|
->get(['sumber_air'])
|
||||||
|
->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->sumber_air))
|
||||||
|
->filter()
|
||||||
|
->countBy()
|
||||||
|
->sortDesc();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'labels' => $rows->keys()->values()->all(),
|
||||||
|
'series' => $rows->values()->map(fn ($value) => (int) $value)->values()->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function kondisiTanamanChart(array $filters): array
|
private function kondisiTanamanChart(array $filters): array
|
||||||
{
|
{
|
||||||
$rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
|
$rows = $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
|
||||||
@@ -572,6 +824,415 @@ class IndikatorPertanianController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function tanamanProduktifCount(array $filters): int
|
||||||
|
{
|
||||||
|
return (int) $this->applyTanamanTableFilters(DB::table('map_tanaman'), $filters)
|
||||||
|
->whereNull('map_tanaman.deleted_at')
|
||||||
|
->where('map_tanaman.kondisi_tanaman', 'baik')
|
||||||
|
->count('map_tanaman.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function batchPanenCount(array $filters): int
|
||||||
|
{
|
||||||
|
$produksiCount = $this->applyProduksiFilters(Produksi::query(), $filters)->count();
|
||||||
|
$surveyPanenCount = $this->applySurveyPanenFilters(SurveyPanen::query(), $filters)->count();
|
||||||
|
|
||||||
|
return (int) $produksiCount + (int) $surveyPanenCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function productionSummaryDescription(array $filters): string
|
||||||
|
{
|
||||||
|
$chart = $this->produksiPerKomoditasChart($filters);
|
||||||
|
|
||||||
|
if (empty($chart['series'])) {
|
||||||
|
return 'Belum ada produksi aktual per komoditas';
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($chart['labels'])
|
||||||
|
->map(fn ($label, $index) => $label.': '.number_format((float) ($chart['series'][$index] ?? 0), 0, ',', '.').' kg')
|
||||||
|
->take(3)
|
||||||
|
->implode(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function commodityFocusData(array $filters): array
|
||||||
|
{
|
||||||
|
$focusCommodities = ['Kopi', 'Kakao', 'Vanili', 'Kemiri'];
|
||||||
|
$rows = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->leftJoin('master_komoditas as k', 'k.id', '=', 'map_lahan.komoditas_id')
|
||||||
|
->whereIn(DB::raw('LOWER(k.nama)'), collect($focusCommodities)->map(fn ($name) => strtolower($name))->all())
|
||||||
|
->selectRaw('k.nama as label')
|
||||||
|
->selectRaw('COUNT(map_lahan.id) as total')
|
||||||
|
->groupBy('k.nama')
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(fn ($row) => [strtolower((string) $row->label) => (int) $row->total]);
|
||||||
|
|
||||||
|
return collect($focusCommodities)
|
||||||
|
->map(fn ($name) => [
|
||||||
|
'label' => $name,
|
||||||
|
'total' => $rows[strtolower($name)] ?? 0,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function farmMapFeatures(array $filters): array
|
||||||
|
{
|
||||||
|
return $this->applyLahanFilters(
|
||||||
|
Lahan::query()->with(['desaKelurahan.kecamatan', 'komoditas']),
|
||||||
|
$filters
|
||||||
|
)
|
||||||
|
->where(function ($query) {
|
||||||
|
$query
|
||||||
|
->whereNotNull('lat')
|
||||||
|
->orWhereNotNull('long')
|
||||||
|
->orWhereNotNull('path');
|
||||||
|
})
|
||||||
|
->limit(250)
|
||||||
|
->get()
|
||||||
|
->map(fn (Lahan $lahan) => [
|
||||||
|
'id' => $lahan->id,
|
||||||
|
'name' => $lahan->nama,
|
||||||
|
'commodity' => $lahan->komoditas?->nama ?? 'Belum ditentukan',
|
||||||
|
'area' => (float) $lahan->luas_lahan,
|
||||||
|
'village' => $lahan->desaKelurahan?->nama,
|
||||||
|
'district' => $lahan->desaKelurahan?->kecamatan?->nama,
|
||||||
|
'lat' => is_numeric($lahan->lat) ? (float) $lahan->lat : null,
|
||||||
|
'lng' => is_numeric($lahan->long) ? (float) $lahan->long : null,
|
||||||
|
'path' => is_array($lahan->path) ? $lahan->path : null,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapLevelData(array $filters): array
|
||||||
|
{
|
||||||
|
$lahanSummaryByDesa = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->whereNotNull('desa_kelurahan_id')
|
||||||
|
->select('desa_kelurahan_id')
|
||||||
|
->selectRaw('COUNT(*) as total_lahan')
|
||||||
|
->selectRaw('COALESCE(SUM(luas_lahan), 0) as total_luas')
|
||||||
|
->groupBy('desa_kelurahan_id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$desaIds = $lahanSummaryByDesa->pluck('desa_kelurahan_id')->filter()->unique()->values();
|
||||||
|
|
||||||
|
if ($filters['desa'] !== 'all') {
|
||||||
|
$desaIds->push($filters['desa']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$desaList = DesaKelurahan::query()
|
||||||
|
->select('id', 'nama', 'path', 'kecamatan_id')
|
||||||
|
->when($desaIds->isNotEmpty(), fn ($query) => $query->whereIn('id', $desaIds->unique()->values()))
|
||||||
|
->with([
|
||||||
|
'kecamatan:id,nama,path,kabupaten_kota_id',
|
||||||
|
'kecamatan.kabupatenKota:id,nama,path,provinsi_id',
|
||||||
|
'kecamatan.kabupatenKota.provinsi:id,nama,path',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
$geoProvinsi = [];
|
||||||
|
$geoKabupaten = [];
|
||||||
|
$geoKecamatan = [];
|
||||||
|
$geoDesa = [];
|
||||||
|
$geoLahan = [];
|
||||||
|
|
||||||
|
foreach ($lahanSummaryByDesa as $summary) {
|
||||||
|
$desa = $desaList->get($summary->desa_kelurahan_id);
|
||||||
|
$kec = $desa?->kecamatan;
|
||||||
|
$kab = $kec?->kabupatenKota;
|
||||||
|
$prov = $kab?->provinsi;
|
||||||
|
$totalLahan = (int) $summary->total_lahan;
|
||||||
|
$totalLuas = (float) $summary->total_luas;
|
||||||
|
|
||||||
|
if ($prov && $prov->path) {
|
||||||
|
$this->accumulateGeoLevel($geoProvinsi, $prov->id, null, $prov->nama, $prov->path, $totalLahan, $totalLuas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kab && $kab->path) {
|
||||||
|
$this->accumulateGeoLevel($geoKabupaten, $kab->id, $prov?->id, $kab->nama, $kab->path, $totalLahan, $totalLuas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kec && $kec->path) {
|
||||||
|
$this->accumulateGeoLevel($geoKecamatan, $kec->id, $kab?->id, $kec->nama, $kec->path, $totalLahan, $totalLuas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($desa && $desa->path) {
|
||||||
|
$this->accumulateGeoLevel($geoDesa, $desa->id, $kec?->id, $desa->nama, $desa->path, $totalLahan, $totalLuas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureSelectedRegionOnMap($filters, $geoProvinsi, $geoKabupaten, $geoKecamatan, $geoDesa);
|
||||||
|
|
||||||
|
$mapLahan = $this->applyLahanFilters(
|
||||||
|
Lahan::query()->with([
|
||||||
|
'profile:id,nama',
|
||||||
|
'petani:id,nama',
|
||||||
|
'komoditas:id,nama',
|
||||||
|
'tanaman:id,lahan_id,komoditas_id',
|
||||||
|
'tanaman.komoditas:id,nama',
|
||||||
|
]),
|
||||||
|
$filters
|
||||||
|
)
|
||||||
|
->whereNotNull('path')
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->limit(500)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($mapLahan as $lahan) {
|
||||||
|
if (! $this->hasMapPolygon($lahan->path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tanaman = $lahan->tanaman
|
||||||
|
->map(fn ($tanaman) => $tanaman->komoditas?->nama)
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ($tanaman->isEmpty() && $lahan->komoditas?->nama) {
|
||||||
|
$tanaman = collect([$lahan->komoditas->nama]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$geoLahan[] = [
|
||||||
|
'id' => $lahan->id,
|
||||||
|
'parent_id' => $lahan->desa_kelurahan_id,
|
||||||
|
'polygon' => $lahan->path,
|
||||||
|
'nama_petani' => $lahan->petani?->nama ?? $lahan->profile?->nama ?? 'N/A',
|
||||||
|
'luas' => (float) $lahan->luas_lahan,
|
||||||
|
'tanaman' => $tanaman,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provinsi' => array_values($geoProvinsi),
|
||||||
|
'kabupaten' => array_values($geoKabupaten),
|
||||||
|
'kecamatan' => array_values($geoKecamatan),
|
||||||
|
'desa' => array_values($geoDesa),
|
||||||
|
'lahan' => $geoLahan,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function accumulateGeoLevel(array &$bucket, int|string $id, int|string|null $parentId, string $name, mixed $polygon, int $totalLahan, float $totalLuas): void
|
||||||
|
{
|
||||||
|
if (! isset($bucket[$id])) {
|
||||||
|
$bucket[$id] = [
|
||||||
|
'id' => $id,
|
||||||
|
'parent_id' => $parentId,
|
||||||
|
'nama' => $name,
|
||||||
|
'polygon' => $polygon,
|
||||||
|
'total_lahan' => 0,
|
||||||
|
'total_luas' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucket[$id]['total_lahan'] += $totalLahan;
|
||||||
|
$bucket[$id]['total_luas'] += $totalLuas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureSelectedRegionOnMap(array $filters, array &$geoProvinsi, array &$geoKabupaten, array &$geoKecamatan, array &$geoDesa): void
|
||||||
|
{
|
||||||
|
$kabupaten = $filters['kabupaten'] !== 'all'
|
||||||
|
? KabupatenKota::query()->with('provinsi:id,nama,path')->find($filters['kabupaten'])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($kabupaten && $kabupaten->path && ! isset($geoKabupaten[$kabupaten->id])) {
|
||||||
|
$this->accumulateGeoLevel($geoKabupaten, $kabupaten->id, $kabupaten->provinsi_id, $kabupaten->nama, $kabupaten->path, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($kabupaten?->provinsi && $kabupaten->provinsi->path && ! isset($geoProvinsi[$kabupaten->provinsi->id])) {
|
||||||
|
$this->accumulateGeoLevel($geoProvinsi, $kabupaten->provinsi->id, null, $kabupaten->provinsi->nama, $kabupaten->provinsi->path, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filters['kecamatan'] !== 'all') {
|
||||||
|
$kecamatan = Kecamatan::query()->find($filters['kecamatan']);
|
||||||
|
|
||||||
|
if ($kecamatan && $kecamatan->path && ! isset($geoKecamatan[$kecamatan->id])) {
|
||||||
|
$this->accumulateGeoLevel($geoKecamatan, $kecamatan->id, $kecamatan->kabupaten_kota_id, $kecamatan->nama, $kecamatan->path, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filters['desa'] !== 'all') {
|
||||||
|
$desa = DesaKelurahan::query()->find($filters['desa']);
|
||||||
|
|
||||||
|
if ($desa && $desa->path && ! isset($geoDesa[$desa->id])) {
|
||||||
|
$this->accumulateGeoLevel($geoDesa, $desa->id, $desa->kecamatan_id, $desa->nama, $desa->path, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasMapPolygon(mixed $path): bool
|
||||||
|
{
|
||||||
|
if (is_array($path)) {
|
||||||
|
return ! empty($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($path) || blank($path) || strtolower($path) === 'null') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($path, true);
|
||||||
|
|
||||||
|
return json_last_error() === JSON_ERROR_NONE && ! empty($decoded) && $decoded !== 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shadowTreeSummary(array $filters): array
|
||||||
|
{
|
||||||
|
$items = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->get(['tanaman_penaung'])
|
||||||
|
->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->tanaman_penaung))
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $items->count(),
|
||||||
|
'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function companionPlantSummary(array $filters): array
|
||||||
|
{
|
||||||
|
$items = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->get(['id', 'komoditas_lain'])
|
||||||
|
->flatMap(fn (Lahan $lahan) => $this->arrayValues($lahan->komoditas_lain))
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $items->count(),
|
||||||
|
'types' => $items->unique(fn ($item) => strtolower((string) $item))->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function plantDiversityAverage(array $filters): float
|
||||||
|
{
|
||||||
|
$lahan = $this->applyLahanFilters(Lahan::query(), $filters)
|
||||||
|
->get(['komoditas_id', 'komoditas_lain', 'tanaman_penaung']);
|
||||||
|
|
||||||
|
if ($lahan->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $lahan->avg(function (Lahan $item) {
|
||||||
|
$total = $item->komoditas_id ? 1 : 0;
|
||||||
|
$total += count($this->arrayValues($item->komoditas_lain));
|
||||||
|
$total += count($this->arrayValues($item->tanaman_penaung));
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sourceWaterSummary(array $filters): string
|
||||||
|
{
|
||||||
|
$chart = $this->sumberAirChart($filters);
|
||||||
|
|
||||||
|
if (empty($chart['labels'])) {
|
||||||
|
return 'Belum ada data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($chart['labels'])
|
||||||
|
->map(fn ($label, $index) => $label.' ('.number_format((int) ($chart['series'][$index] ?? 0), 0, ',', '.').')')
|
||||||
|
->take(2)
|
||||||
|
->implode(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waterManagementSummary(array $filters): string
|
||||||
|
{
|
||||||
|
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||||
|
->whereNotNull('kondisi_pengelolaan_air')
|
||||||
|
->pluck('kondisi_pengelolaan_air')
|
||||||
|
->map(fn ($value) => $this->waterManagementShortLabel((int) $value))
|
||||||
|
->countBy()
|
||||||
|
->sortDesc();
|
||||||
|
|
||||||
|
return $this->topCountSummary($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seasonalWaterNeedSummary(array $filters): string
|
||||||
|
{
|
||||||
|
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||||
|
->whereNotNull('kebutuhan_air_permusim')
|
||||||
|
->pluck('kebutuhan_air_permusim')
|
||||||
|
->map(fn ($value) => $this->shortEnumLabel((string) $value))
|
||||||
|
->countBy()
|
||||||
|
->sortDesc();
|
||||||
|
|
||||||
|
return $this->topCountSummary($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function soilConservationSummary(array $filters): string
|
||||||
|
{
|
||||||
|
$rows = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||||
|
->get(['kebiasaan_konservasi_tanah'])
|
||||||
|
->flatMap(fn (PraktikBudidaya $praktik) => $this->arrayValues($praktik->kebiasaan_konservasi_tanah))
|
||||||
|
->filter()
|
||||||
|
->countBy()
|
||||||
|
->sortDesc();
|
||||||
|
|
||||||
|
return $this->topCountSummary($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function soilConservationScoreSummary(array $filters): string
|
||||||
|
{
|
||||||
|
$average = $this->applyPraktikBudidayaFilters(PraktikBudidaya::query(), $filters)
|
||||||
|
->whereNotNull('nilai_konservasi_tanah')
|
||||||
|
->avg('nilai_konservasi_tanah');
|
||||||
|
|
||||||
|
if ($average === null) {
|
||||||
|
return 'Belum ada data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Rata-rata '.number_format((float) $average, 1, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function topCountSummary($rows): string
|
||||||
|
{
|
||||||
|
if ($rows->isEmpty()) {
|
||||||
|
return 'Belum ada data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows
|
||||||
|
->map(fn ($count, $label) => $label.' ('.number_format((int) $count, 0, ',', '.').')')
|
||||||
|
->take(2)
|
||||||
|
->implode(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waterManagementShortLabel(int $value): string
|
||||||
|
{
|
||||||
|
return match (KondisiPengelolaanAir::tryFrom($value)) {
|
||||||
|
KondisiPengelolaanAir::KURANG_TIDAK_ADA_UPAYA => 'Kurang, tanpa upaya',
|
||||||
|
KondisiPengelolaanAir::KURANG_TIDAK_SUDAH_BERUPAYA => 'Kurang, ada upaya',
|
||||||
|
KondisiPengelolaanAir::CUKUP_DARI_HUJAN => 'Cukup dari hujan',
|
||||||
|
KondisiPengelolaanAir::CUKUP_IRIGASI_PINJAM => 'Cukup, irigasi pinjam',
|
||||||
|
KondisiPengelolaanAir::CUKUP_IRIGASI_SENDIRI => 'Cukup, irigasi sendiri',
|
||||||
|
default => 'Belum ditentukan',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function arrayValues(mixed $value): array
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
return collect($value)
|
||||||
|
->flatten()
|
||||||
|
->filter(fn ($item) => filled($item))
|
||||||
|
->map(fn ($item) => is_scalar($item) ? (string) $item : json_encode($item))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && filled($value) && strtolower($value) !== 'null') {
|
||||||
|
$decoded = json_decode($value, true);
|
||||||
|
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
return $this->arrayValues($decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
private function chartFromRows($rows, callable $labelResolver): array
|
private function chartFromRows($rows, callable $labelResolver): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -676,6 +1337,13 @@ class IndikatorPertanianController extends Controller
|
|||||||
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']));
|
->when($filters['komoditas'] !== 'all', fn ($query) => $query->where('komoditas_id', $filters['komoditas']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function applySurveyPanenFilters($query, array $filters)
|
||||||
|
{
|
||||||
|
return $query->whereHas('petani', function ($petani) use ($filters) {
|
||||||
|
$this->applyPetaniFilters($petani, $filters);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function applyTanamanTableFilters($query, array $filters)
|
private function applyTanamanTableFilters($query, array $filters)
|
||||||
{
|
{
|
||||||
return $query
|
return $query
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Dashboard;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\AlphaVantageMarket;
|
||||||
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
|
use Illuminate\Http\Client\Pool;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class InternationalMarketController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke()
|
||||||
|
{
|
||||||
|
abort_unless(Gate::allows('super_admin') || Gate::allows('admin') || Gate::allows('koordinator') || Gate::allows('enumerator'), 403);
|
||||||
|
|
||||||
|
// File cache keeps external quotes independent of the application database.
|
||||||
|
$cache = Cache::store('file');
|
||||||
|
$marketData = $cache->remember('market.yahoo.quotes.v3', 300, function () use ($cache) {
|
||||||
|
$commodities = [
|
||||||
|
['name' => 'Kopi Arabica', 'symbol' => 'KC=F'],
|
||||||
|
['name' => 'Kakao (Cocoa)', 'symbol' => 'CC=F'],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$responses = Http::pool(function (Pool $pool) use ($commodities) {
|
||||||
|
return array_map(fn ($commodity) => $pool->as($commodity['symbol'])
|
||||||
|
->acceptJson()->withHeaders(['User-Agent' => 'Mozilla/5.0'])
|
||||||
|
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||||
|
->connectTimeout(3)->timeout(8)
|
||||||
|
->get('https://query1.finance.yahoo.com/v8/finance/chart/'.rawurlencode($commodity['symbol']), [
|
||||||
|
'interval' => '1d', 'range' => '5d',
|
||||||
|
]), [...$commodities, ['symbol' => 'IDR=X']]);
|
||||||
|
});
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
report($exception);
|
||||||
|
$responses = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$exchangeRate = $cache->get('market.yahoo.usd_idr');
|
||||||
|
if (is_array($exchangeRate)) {
|
||||||
|
$exchangeRate['stale'] = true;
|
||||||
|
}
|
||||||
|
$rateResponse = $responses['IDR=X'] ?? null;
|
||||||
|
if ($rateResponse instanceof \Illuminate\Http\Client\Response && $rateResponse->successful()) {
|
||||||
|
$meta = $rateResponse->json('chart.result.0.meta');
|
||||||
|
if (is_array($meta) && ($meta['symbol'] ?? null) === 'IDR=X'
|
||||||
|
&& ($meta['currency'] ?? null) === 'IDR'
|
||||||
|
&& is_numeric($meta['regularMarketPrice'] ?? null) && $meta['regularMarketPrice'] > 0
|
||||||
|
&& is_numeric($meta['regularMarketTime'] ?? null)) {
|
||||||
|
$exchangeRate = [
|
||||||
|
'value' => (float) $meta['regularMarketPrice'],
|
||||||
|
'quoted_at' => gmdate('c', (int) $meta['regularMarketTime']),
|
||||||
|
'source' => 'Yahoo Finance',
|
||||||
|
'source_url' => 'https://finance.yahoo.com/quote/IDR%3DX/',
|
||||||
|
'stale' => false,
|
||||||
|
];
|
||||||
|
$cache->put('market.yahoo.usd_idr', $exchangeRate, now()->addDays(7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$quotes = array_map(function ($commodity) use ($responses, $cache) {
|
||||||
|
$symbol = $commodity['symbol'];
|
||||||
|
$key = 'market.yahoo.last.'.rawurlencode($symbol);
|
||||||
|
$quote = $commodity + [
|
||||||
|
'source' => 'Yahoo Finance',
|
||||||
|
'source_url' => 'https://finance.yahoo.com/quote/'.rawurlencode($symbol).'/',
|
||||||
|
'price' => null, 'currency' => null, 'unit' => null,
|
||||||
|
'quoted_at' => null, 'stale' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $responses[$symbol] ?? null;
|
||||||
|
if (!$response instanceof \Illuminate\Http\Client\Response || !$response->successful()) {
|
||||||
|
throw new \RuntimeException('Quote unavailable');
|
||||||
|
}
|
||||||
|
$meta = $response->json('chart.result.0.meta');
|
||||||
|
if ($response->json('chart.error') || !is_array($meta)
|
||||||
|
|| ($meta['symbol'] ?? null) !== $symbol
|
||||||
|
|| !is_numeric($meta['regularMarketPrice'] ?? null)
|
||||||
|
|| (float) $meta['regularMarketPrice'] <= 0
|
||||||
|
|| !is_numeric($meta['regularMarketTime'] ?? null)
|
||||||
|
|| !is_string($meta['currency'] ?? null)) {
|
||||||
|
throw new \RuntimeException('Invalid quote');
|
||||||
|
}
|
||||||
|
|
||||||
|
$quote['price'] = (float) $meta['regularMarketPrice'];
|
||||||
|
$quote['currency'] = $meta['currency'];
|
||||||
|
// Yahoo quotes Arabica in US cents/lb; cocoa and robusta in USD/tonne.
|
||||||
|
// Apply the unit only when the API currency matches that convention.
|
||||||
|
$quote['unit'] = match (true) {
|
||||||
|
$symbol === 'KC=F' && $meta['currency'] === 'USX' => 'sen AS / lb',
|
||||||
|
$symbol === 'CC=F' && $meta['currency'] === 'USD' => 'USD / ton metrik',
|
||||||
|
default => $meta['currency'].' (satuan mengikuti sumber)',
|
||||||
|
};
|
||||||
|
$quote['quoted_at'] = gmdate('c', (int) $meta['regularMarketTime']);
|
||||||
|
$cache->put($key, $quote, now()->addDays(7));
|
||||||
|
|
||||||
|
return $quote;
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$previous = $cache->get($key);
|
||||||
|
if (is_array($previous)) {
|
||||||
|
$previous['stale'] = true;
|
||||||
|
return $previous;
|
||||||
|
}
|
||||||
|
return $quote;
|
||||||
|
}
|
||||||
|
}, $commodities);
|
||||||
|
|
||||||
|
return ['quotes' => $quotes, 'exchange_rate' => $exchangeRate];
|
||||||
|
});
|
||||||
|
|
||||||
|
$quotes = $marketData['quotes'];
|
||||||
|
$exchangeRate = $marketData['exchange_rate'];
|
||||||
|
foreach ($quotes as &$quote) {
|
||||||
|
$quote['id'] = 'yahoo:'.$quote['symbol'];
|
||||||
|
$quote['description'] = 'Estimasi konversi harga kontrak berjangka internasional.';
|
||||||
|
$quote['period'] = $quote['quoted_at'];
|
||||||
|
$quote['frequency'] = 'market';
|
||||||
|
}
|
||||||
|
unset($quote);
|
||||||
|
$quotes = array_merge($quotes, $this->imfQuotes($cache), app(AlphaVantageMarket::class)->quotes($cache));
|
||||||
|
$quotes = array_values(array_filter($quotes, fn ($quote) => is_numeric($quote['price']) && is_finite((float) $quote['price']) && $quote['price'] > 0));
|
||||||
|
foreach ($quotes as &$quote) {
|
||||||
|
// All active providers quote either US cents/lb or USD/metric tonne.
|
||||||
|
$usdPerKg = match ($quote['unit']) {
|
||||||
|
'sen AS / lb' => $quote['price'] / 100 / 0.45359237,
|
||||||
|
'USD / ton metrik' => $quote['price'] / 1000,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
$quote['price_idr_per_kg'] = $exchangeRate && $usdPerKg !== null
|
||||||
|
? round($usdPerKg * $exchangeRate['value'], 2) : null;
|
||||||
|
unset($quote['usd_per_kg']); // Old Alpha cache entries may contain this field.
|
||||||
|
}
|
||||||
|
unset($quote);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'quotes' => $quotes,
|
||||||
|
'exchange_rate' => $exchangeRate,
|
||||||
|
])->header('Cache-Control', 'no-store');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function imfQuotes(Repository $cache): array
|
||||||
|
{
|
||||||
|
// IMF observations are monthly averages; fetch at most once a day.
|
||||||
|
return $cache->remember('market.imf.monthly.v1', 86400, function () use ($cache) {
|
||||||
|
try {
|
||||||
|
$response = Http::withHeaders(['Accept' => 'text/csv', 'User-Agent' => 'Mozilla/5.0'])
|
||||||
|
->withOptions(['verify' => config('services.yahoo_finance.ca_bundle') ?: true])
|
||||||
|
->connectTimeout(3)->timeout(10)
|
||||||
|
->get('https://api.imf.org/external/sdmx/2.1/data/IMF.RES,PCPS/G001.PCOFFOTM+PCOFFROB+PCOCO.USD.M', [
|
||||||
|
'startPeriod' => now()->subYears(2)->format('Y-m'), 'format' => 'csvfile',
|
||||||
|
])->throw();
|
||||||
|
$stream = fopen('php://memory', 'r+');
|
||||||
|
try {
|
||||||
|
fwrite($stream, $response->body());
|
||||||
|
rewind($stream);
|
||||||
|
$headers = fgetcsv($stream, 0, ',', '"', '');
|
||||||
|
if (!$headers || array_diff(['COUNTRY', 'INDICATOR', 'DATA_TRANSFORMATION', 'FREQUENCY', 'TIME_PERIOD', 'OBS_VALUE', 'SCALE'], $headers)) {
|
||||||
|
throw new \RuntimeException('Unexpected IMF CSV format');
|
||||||
|
}
|
||||||
|
$latest = [];
|
||||||
|
while (($values = fgetcsv($stream, 0, ',', '"', '')) !== false) {
|
||||||
|
if (count($values) !== count($headers)) continue;
|
||||||
|
$row = array_combine($headers, $values);
|
||||||
|
if ($row['COUNTRY'] !== 'G001' || $row['DATA_TRANSFORMATION'] !== 'USD' || $row['FREQUENCY'] !== 'M'
|
||||||
|
|| $row['SCALE'] !== '0' || !is_numeric($row['OBS_VALUE']) || $row['OBS_VALUE'] <= 0
|
||||||
|
|| !preg_match('/^\d{4}-M(0[1-9]|1[0-2])$/', $row['TIME_PERIOD'])) continue;
|
||||||
|
$indicator = $row['INDICATOR'];
|
||||||
|
if (!isset($latest[$indicator]) || $row['TIME_PERIOD'] > $latest[$indicator]['TIME_PERIOD']) {
|
||||||
|
$latest[$indicator] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fclose($stream);
|
||||||
|
}
|
||||||
|
$definitions = [
|
||||||
|
'PCOFFOTM' => ['Kopi Arabica', 'USX', 'sen AS / lb', 'Biji kopi Arabica (Other Milds). Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||||
|
'PCOFFROB' => ['Kopi Robusta', 'USX', 'sen AS / lb', 'Biji kopi Robusta. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||||
|
'PCOCO' => ['Kakao (Cocoa)', 'USD', 'USD / ton metrik', 'Biji kakao. Estimasi konversi harga acuan rata-rata bulanan IMF.'],
|
||||||
|
];
|
||||||
|
$quotes = [];
|
||||||
|
foreach ($definitions as $indicator => [$name, $currency, $unit, $description]) {
|
||||||
|
if (!isset($latest[$indicator])) continue;
|
||||||
|
$row = $latest[$indicator];
|
||||||
|
$quotes[] = [
|
||||||
|
'id' => 'imf:'.$indicator, 'symbol' => $indicator, 'name' => $name,
|
||||||
|
'price' => (float) $row['OBS_VALUE'], 'currency' => $currency, 'unit' => $unit,
|
||||||
|
'source' => 'IMF · PCPS', 'source_url' => 'https://data.imf.org/Datasets/PCPS',
|
||||||
|
'description' => $description, 'frequency' => 'monthly',
|
||||||
|
'period' => str_replace('-M', '-', $row['TIME_PERIOD']), 'stale' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (!$quotes) throw new \RuntimeException('No valid IMF observations');
|
||||||
|
$cache->put('market.imf.last.v1', $quotes, now()->addDays(30));
|
||||||
|
return $quotes;
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
report($exception);
|
||||||
|
return array_map(function ($quote) {
|
||||||
|
$quote['stale'] = true;
|
||||||
|
return $quote;
|
||||||
|
}, $cache->get('market.imf.last.v1', []));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ class ProfilPetaniController extends Controller
|
|||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
$title = 'Dashboard Profil Petani';
|
$title = 'Dashboard Profil Petani';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ use BackedEnum;
|
|||||||
|
|
||||||
class SurveyPetaniController extends Controller
|
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()
|
public function index()
|
||||||
{
|
{
|
||||||
$colors = $this->colors();
|
$colors = $this->colors();
|
||||||
@@ -31,7 +31,7 @@ class SurveyPetaniController extends Controller
|
|||||||
// Progres per enumerator
|
// Progres per enumerator
|
||||||
|
|
||||||
$enumerators = User::query()
|
$enumerators = User::query()
|
||||||
->where('role', 'fasilitator')
|
->where('role', 'enumerator')
|
||||||
->withCount([
|
->withCount([
|
||||||
'petaniDibuat as done',
|
'petaniDibuat as done',
|
||||||
])
|
])
|
||||||
@@ -489,34 +489,34 @@ class SurveyPetaniController extends Controller
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
'color' => '#28a745',
|
'color' => '#6FD68F',
|
||||||
'bg' => 'rgba(40,167,69,.15)',
|
'bg' => 'rgba(111,214,143,.24)',
|
||||||
'tx' => '#28a745',
|
'tx' => '#6FD68F',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'color' => '#007bff',
|
'color' => '#5FC7D6',
|
||||||
'bg' => 'rgba(0,123,255,.15)',
|
'bg' => 'rgba(95,199,214,.24)',
|
||||||
'tx' => '#007bff',
|
'tx' => '#5FC7D6',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'color' => '#fd7e14',
|
'color' => '#FFB46F',
|
||||||
'bg' => 'rgba(253,126,20,.15)',
|
'bg' => 'rgba(255,180,111,.26)',
|
||||||
'tx' => '#fd7e14',
|
'tx' => '#FFB46F',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'color' => '#dc3545',
|
'color' => '#F47C86',
|
||||||
'bg' => 'rgba(220,53,69,.15)',
|
'bg' => 'rgba(244,124,134,.26)',
|
||||||
'tx' => '#dc3545',
|
'tx' => '#F47C86',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'color' => '#6f42c1',
|
'color' => '#9D8EF0',
|
||||||
'bg' => 'rgba(111,66,193,.15)',
|
'bg' => 'rgba(157,142,240,.26)',
|
||||||
'tx' => '#6f42c1',
|
'tx' => '#9D8EF0',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'color' => '#20c997',
|
'color' => '#65D8BD',
|
||||||
'bg' => 'rgba(32,201,151,.15)',
|
'bg' => 'rgba(101,216,189,.26)',
|
||||||
'tx' => '#20c997',
|
'tx' => '#65D8BD',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class PetaniCPMController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class PetaniCPMController extends Controller
|
|||||||
|
|
||||||
public function create(Request $request): View
|
public function create(Request $request): View
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class PetaniCPMController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class PetaniCPMController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $id): View
|
public function show(string $id): View
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class PetaniCPMController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function edit(Request $request, string $id): View
|
public function edit(Request $request, string $id): View
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ class PetaniCPMController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function update(Request $request, string $id)
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ class PetaniCPMController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $id)
|
public function destroy(string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class ValueBasisDataController extends Controller
|
|||||||
|
|
||||||
public function index(Request $request, string $tabelId): View
|
public function index(Request $request, string $tabelId): View
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Konfigurasi;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengatur komoditas yang ditangani seorang koordinator.
|
||||||
|
*/
|
||||||
|
class KoordinatorKomoditasController extends Controller
|
||||||
|
{
|
||||||
|
public function update(Request $request, User $user)
|
||||||
|
{
|
||||||
|
abort_unless(Gate::any(['super_admin', 'admin']), 403);
|
||||||
|
|
||||||
|
if (! $user->isKoordinator()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Komoditas hanya bisa diatur untuk user dengan role Koordinator.',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'komoditas_ids' => ['required', 'array', 'min:1'],
|
||||||
|
'komoditas_ids.*' => ['integer', 'exists:master_komoditas,id'],
|
||||||
|
], [
|
||||||
|
'komoditas_ids.required' => 'Pilih minimal satu komoditas.',
|
||||||
|
'komoditas_ids.min' => 'Pilih minimal satu komoditas.',
|
||||||
|
'komoditas_ids.*.exists' => 'Komoditas yang dipilih tidak valid.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::simpanPenugasan($user, $validated['komoditas_ids']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Komoditas koordinator berhasil diperbarui.',
|
||||||
|
'data' => $user->komoditasKoordinatorIds(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menyamakan penugasan koordinator dengan daftar komoditas yang dipilih.
|
||||||
|
* Penugasan yang dilepas di-soft delete, penugasan baru dibuat.
|
||||||
|
*/
|
||||||
|
public static function simpanPenugasan(User $user, array $komoditasIds): void
|
||||||
|
{
|
||||||
|
$komoditasIds = array_values(array_unique(array_map('intval', $komoditasIds)));
|
||||||
|
|
||||||
|
DB::transaction(function () use ($user, $komoditasIds) {
|
||||||
|
$user->koordinatorKomoditas()
|
||||||
|
->whereNotIn('komoditas_id', $komoditasIds)
|
||||||
|
->get()
|
||||||
|
->each->delete();
|
||||||
|
|
||||||
|
$sudahAda = $user->komoditasKoordinatorIds();
|
||||||
|
|
||||||
|
foreach (array_diff($komoditasIds, $sudahAda) as $komoditasId) {
|
||||||
|
$user->koordinatorKomoditas()->create([
|
||||||
|
'komoditas_id' => $komoditasId,
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
'updated_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,29 @@ use App\Models\User;
|
|||||||
use App\Models\Profile;
|
use App\Models\Profile;
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Models\Wilayah\Provinsi;
|
use App\Models\Wilayah\Provinsi;
|
||||||
|
use App\Models\Pertanian\Komoditas;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class UserController extends Controller
|
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)
|
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');
|
$tab = $request->get('tab', 'active');
|
||||||
if ($tab === 'pending') {
|
if ($tab === 'pending') {
|
||||||
@@ -45,8 +58,15 @@ class UserController extends Controller
|
|||||||
$pendingUsers = User::whereNull('email_verified_at')->count();
|
$pendingUsers = User::whereNull('email_verified_at')->count();
|
||||||
|
|
||||||
$roles = Role::cases();
|
$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)
|
public function verify(Request $request, $id)
|
||||||
@@ -92,8 +112,9 @@ class UserController extends Controller
|
|||||||
{
|
{
|
||||||
$roles = Role::cases();
|
$roles = Role::cases();
|
||||||
$provinsi = Provinsi::orderBy('nama', 'asc')->get();
|
$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)
|
public function store(Request $request)
|
||||||
@@ -104,6 +125,8 @@ class UserController extends Controller
|
|||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
|
||||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||||
'role' => ['required', 'string'],
|
'role' => ['required', 'string'],
|
||||||
|
'komoditas_ids' => ['nullable', 'array', 'required_if:role,' . Role::KOORDINATOR->value],
|
||||||
|
'komoditas_ids.*' => ['integer', 'exists:master_komoditas,id'],
|
||||||
'telepon' => ['required', 'max:13'],
|
'telepon' => ['required', 'max:13'],
|
||||||
'desa_kelurahan_id' => ['nullable', 'exists:desa_kelurahan,id'],
|
'desa_kelurahan_id' => ['nullable', 'exists:desa_kelurahan,id'],
|
||||||
'ktp' => ['nullable', 'min:16', 'max:16'],
|
'ktp' => ['nullable', 'min:16', 'max:16'],
|
||||||
@@ -116,6 +139,7 @@ class UserController extends Controller
|
|||||||
'password.confirmed' => 'Konfirmasi password tidak cocok!',
|
'password.confirmed' => 'Konfirmasi password tidak cocok!',
|
||||||
'password.min' => 'Password minimal 8 karakter!',
|
'password.min' => 'Password minimal 8 karakter!',
|
||||||
'email.unique' => 'Email sudah terdaftar.',
|
'email.unique' => 'Email sudah terdaftar.',
|
||||||
|
'komoditas_ids.required_if' => 'Pilih minimal satu komoditas untuk koordinator.',
|
||||||
'required' => 'Kolom :attribute wajib diisi.'
|
'required' => 'Kolom :attribute wajib diisi.'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -163,6 +187,10 @@ class UserController extends Controller
|
|||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($user->isKoordinator()) {
|
||||||
|
KoordinatorKomoditasController::simpanPenugasan($user, $request->input('komoditas_ids', []));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
if ($request->expectsJson()) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class AksesPasarController extends Controller
|
|||||||
{
|
{
|
||||||
$petani = Petani::findOrFail($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();
|
$hargaJualPanen = HargaJualPanen::where('petani_id', $petaniId)->get();
|
||||||
$hargaJualRows = collect([
|
$hargaJualRows = collect([
|
||||||
'Buah segar hasil petik',
|
'Buah segar hasil petik',
|
||||||
@@ -57,11 +57,19 @@ class AksesPasarController extends Controller
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$aksesPasarData = $this->aksesPasarPayload($validated);
|
$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['uid'] = 'AKP-' . Str::uuid();
|
||||||
$aksesPasarData['created_by'] = Auth::id();
|
$aksesPasarData['created_by'] = Auth::id();
|
||||||
$aksesPasarData['updated_by'] = Auth::id();
|
$aksesPasarData['updated_by'] = Auth::id();
|
||||||
|
|
||||||
$aksesPasar = AksesPasar::create($aksesPasarData);
|
$aksesPasar = AksesPasar::create($aksesPasarData);
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->has('harga_jual')) {
|
if ($request->has('harga_jual')) {
|
||||||
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
|
$this->syncHargaJual($request->petani_id, $validated['harga_jual'] ?? [], Auth::id());
|
||||||
@@ -118,7 +126,8 @@ class AksesPasarController extends Controller
|
|||||||
'skorPengalamanBerhentiValues' => [
|
'skorPengalamanBerhentiValues' => [
|
||||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
||||||
]
|
],
|
||||||
|
'sumberInfoLainnyaValue' => 'Lainnya',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +137,17 @@ class AksesPasarController extends Controller
|
|||||||
|
|
||||||
return $request->validate([
|
return $request->validate([
|
||||||
'petani_id' => ['required', 'exists:petani,id'],
|
'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())],
|
'skor_pengalaman' => ['nullable', 'integer', Rule::in(SkorPengalaman::values())],
|
||||||
'alasan_berhenti' => [
|
'alasan_berhenti' => [
|
||||||
Rule::requiredIf(fn () => in_array((int)$request->input('skor_pengalaman'), [
|
Rule::requiredIf(fn () => in_array((int)$request->input('skor_pengalaman'), [
|
||||||
@@ -144,6 +164,14 @@ class AksesPasarController extends Controller
|
|||||||
'nullable', 'string', 'max:255'
|
'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())],
|
'praktik_fermentasi' => ['nullable', 'string', Rule::in(PraktikFermentasi::values())],
|
||||||
'proses_fermentasi' => [
|
'proses_fermentasi' => [
|
||||||
Rule::requiredIf(fn () => filled($request->input('praktik_fermentasi')) && $request->input('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value),
|
Rule::requiredIf(fn () => filled($request->input('praktik_fermentasi')) && $request->input('praktik_fermentasi') !== PraktikFermentasi::BELUM_PERNAH->value),
|
||||||
@@ -168,6 +196,7 @@ class AksesPasarController extends Controller
|
|||||||
], [
|
], [
|
||||||
'petani_id.required' => 'Petani harus dipilih.',
|
'petani_id.required' => 'Petani harus dipilih.',
|
||||||
'petani_id.exists' => 'Petani tidak ditemukan.',
|
'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.integer' => 'Skor pengalaman harus berupa angka.',
|
||||||
'skor_pengalaman.in' => 'Skor pengalaman tidak valid.',
|
'skor_pengalaman.in' => 'Skor pengalaman tidak valid.',
|
||||||
'alasan_berhenti.required' => 'Alasan berhenti harus diisi.',
|
'alasan_berhenti.required' => 'Alasan berhenti harus diisi.',
|
||||||
@@ -204,6 +233,10 @@ class AksesPasarController extends Controller
|
|||||||
{
|
{
|
||||||
$payload = collect($validated)->except(['harga_jual'])->all();
|
$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), [
|
if (!in_array((int)($payload['skor_pengalaman'] ?? 0), [
|
||||||
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
SkorPengalaman::PERNAH_BERHENTI_TIDAK_ADA_AKSES->value,
|
||||||
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
SkorPengalaman::PERNAH_BERHENTI_MUNGKIN_KEMBALI->value
|
||||||
@@ -215,6 +248,10 @@ class AksesPasarController extends Controller
|
|||||||
$payload['saluran_terdahulu_lainnya'] = null;
|
$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) {
|
if (($payload['praktik_fermentasi'] ?? null) === PraktikFermentasi::BELUM_PERNAH->value) {
|
||||||
$payload['proses_fermentasi'] = null;
|
$payload['proses_fermentasi'] = null;
|
||||||
$payload['model_fermentasi_memungkinkan'] = null;
|
$payload['model_fermentasi_memungkinkan'] = null;
|
||||||
@@ -223,7 +260,6 @@ class AksesPasarController extends Controller
|
|||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function syncHargaJual($petaniId, array $hargaJualData, $createdBy, $updatedBy = null): void
|
private function syncHargaJual($petaniId, array $hargaJualData, $createdBy, $updatedBy = null): void
|
||||||
{
|
{
|
||||||
HargaJualPanen::where('petani_id', $petaniId)->delete();
|
HargaJualPanen::where('petani_id', $petaniId)->delete();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Web\Produktivitas;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@@ -26,26 +27,35 @@ class AsetKelompokTaniController extends Controller
|
|||||||
|
|
||||||
private function authorizeAccess(): void
|
private function authorizeAccess(): void
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function redirectToSection(string $url, string $section, string $message)
|
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||||
{
|
{
|
||||||
return redirect($url . '#section-' . $section)->with([
|
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
|
||||||
'success' => $message,
|
'success' => $message,
|
||||||
'success_section' => $section,
|
'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)
|
public function index(KelompokTani $kelompokTani)
|
||||||
{
|
{
|
||||||
$this->authorizeAccess();
|
$this->authorizeAccess();
|
||||||
|
|
||||||
$kelompokTani->load('aset');
|
return redirect($this->kegiatanUrl($kelompokTani) . '#section-aset-baku');
|
||||||
|
|
||||||
return view('produktivitas.survey-kelompok-tani.aset-keuangan', compact('kelompokTani'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request, KelompokTani $kelompokTani)
|
public function update(Request $request, KelompokTani $kelompokTani)
|
||||||
@@ -56,11 +66,26 @@ class AsetKelompokTaniController extends Controller
|
|||||||
'aset' => ['required', 'array'],
|
'aset' => ['required', 'array'],
|
||||||
'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)],
|
'aset.*.jenis_aset' => ['required', Rule::in(self::JENIS_ASET_BAKU)],
|
||||||
'aset.*.status_aset' => ['nullable', 'boolean'],
|
'aset.*.status_aset' => ['nullable', 'boolean'],
|
||||||
'aset.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
|
'aset.*.status_pengelolaan_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
|
||||||
'aset.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
|
'aset.*.kondisi_aset' => ['required_if_accepted:aset.*.status_aset', 'nullable', 'string', 'max:100'],
|
||||||
|
...$this->asetLainnyaRules(),
|
||||||
|
], [
|
||||||
|
'aset.*.status_pengelolaan_aset.required_if_accepted' => 'Status Pengelolaan wajib diisi jika aset dipilih Ada.',
|
||||||
|
'aset.*.kondisi_aset.required_if_accepted' => 'Kondisi wajib diisi jika aset dipilih Ada.',
|
||||||
|
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ($validated['aset'] as $item) {
|
DB::transaction(function () use ($kelompokTani, $validated) {
|
||||||
|
$this->saveAsetBaku($kelompokTani, $validated['aset']);
|
||||||
|
$this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-baku', 'Data Operasional berhasil disimpan.', $this->nextAssetSection($kelompokTani));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveAsetBaku(KelompokTani $kelompokTani, array $items): void
|
||||||
|
{
|
||||||
|
foreach ($items as $item) {
|
||||||
$jenisAset = $item['jenis_aset'];
|
$jenisAset = $item['jenis_aset'];
|
||||||
$ada = (bool) ($item['status_aset'] ?? false);
|
$ada = (bool) ($item['status_aset'] ?? false);
|
||||||
|
|
||||||
@@ -82,8 +107,8 @@ class AsetKelompokTaniController extends Controller
|
|||||||
|
|
||||||
$existing->update([
|
$existing->update([
|
||||||
'status_aset' => true,
|
'status_aset' => true,
|
||||||
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? $existing->status_pengelolaan_aset,
|
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
|
||||||
'kondisi_aset' => $item['kondisi_aset'] ?? $existing->kondisi_aset,
|
'kondisi_aset' => $item['kondisi_aset'],
|
||||||
'deleted_by' => null,
|
'deleted_by' => null,
|
||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
@@ -94,30 +119,40 @@ class AsetKelompokTaniController extends Controller
|
|||||||
'uid' => 'AKT-' . strtoupper(Str::random(10)),
|
'uid' => 'AKT-' . strtoupper(Str::random(10)),
|
||||||
'jenis_aset' => $jenisAset,
|
'jenis_aset' => $jenisAset,
|
||||||
'status_aset' => true,
|
'status_aset' => true,
|
||||||
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'] ?? '-',
|
'status_pengelolaan_aset' => $item['status_pengelolaan_aset'],
|
||||||
'kondisi_aset' => $item['kondisi_aset'] ?? '-',
|
'kondisi_aset' => $item['kondisi_aset'],
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-baku', 'Data aset produktif berhasil disimpan.');
|
private function asetLainnyaRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'aset_lainnya' => ['nullable', 'array'],
|
||||||
|
'aset_lainnya.*.jenis_aset' => ['nullable', 'string', 'max:255', Rule::notIn(self::JENIS_ASET_BAKU)],
|
||||||
|
'aset_lainnya.*.status_pengelolaan_aset' => ['nullable', 'string', 'max:100'],
|
||||||
|
'aset_lainnya.*.kondisi_aset' => ['nullable', 'string', 'max:100'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request, KelompokTani $kelompokTani)
|
public function store(Request $request, KelompokTani $kelompokTani)
|
||||||
{
|
{
|
||||||
$this->authorizeAccess();
|
$this->authorizeAccess();
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate($this->asetLainnyaRules(), [
|
||||||
'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'],
|
|
||||||
], [
|
|
||||||
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
|
'aset_lainnya.*.jenis_aset.not_in' => 'Jenis aset ini sudah ada di daftar baku di atas.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ($validated['aset_lainnya'] ?? [] as $item) {
|
DB::transaction(fn () => $this->createAsetLainnya($kelompokTani, $validated['aset_lainnya'] ?? []));
|
||||||
|
|
||||||
|
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil ditambahkan.', $this->nextAssetSection($kelompokTani));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAsetLainnya(KelompokTani $kelompokTani, array $items): void
|
||||||
|
{
|
||||||
|
foreach ($items as $item) {
|
||||||
$jenisAset = trim($item['jenis_aset'] ?? '');
|
$jenisAset = trim($item['jenis_aset'] ?? '');
|
||||||
|
|
||||||
if ($jenisAset === '') {
|
if ($jenisAset === '') {
|
||||||
@@ -134,8 +169,6 @@ class AsetKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil ditambahkan.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset)
|
public function destroy(KelompokTani $kelompokTani, AsetKelompokTani $aset)
|
||||||
@@ -143,11 +176,11 @@ class AsetKelompokTaniController extends Controller
|
|||||||
$this->authorizeAccess();
|
$this->authorizeAccess();
|
||||||
|
|
||||||
abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404);
|
abort_unless($aset->survey_kelompok_tani_id === $kelompokTani->id, 404);
|
||||||
abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Aset baku diatur lewat checklist, bukan dihapus langsung.');
|
abort_if(in_array($aset->jenis_aset, self::JENIS_ASET_BAKU), 422, 'Operasional: gunakan pilihan Ada/Tidak pada tabel untuk menghapus aset baku.');
|
||||||
|
|
||||||
$aset->update(['deleted_by' => auth()->id()]);
|
$aset->update(['deleted_by' => auth()->id()]);
|
||||||
$aset->delete();
|
$aset->delete();
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'aset-lainnya', 'Aset lainnya berhasil dihapus.');
|
return $this->redirectToSection($this->kegiatanUrl($kelompokTani), 'aset-lainnya', 'Data Operasional berhasil dihapus.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ namespace App\Http\Controllers\Web\Produktivitas;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
use App\Models\Pertanian\KelompokTani;
|
use App\Models\Pertanian\KelompokTani;
|
||||||
use App\Models\Pertanian\BantuanKelompokTani;
|
use App\Models\Pertanian\BantuanKelompokTani;
|
||||||
@@ -14,14 +16,14 @@ class BantuanKelompokTaniController extends Controller
|
|||||||
{
|
{
|
||||||
private function authorizeAccess(): void
|
private function authorizeAccess(): void
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function redirectToSection(string $url, string $section, string $message)
|
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||||
{
|
{
|
||||||
return redirect($url . '#section-' . $section)->with([
|
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
|
||||||
'success' => $message,
|
'success' => $message,
|
||||||
'success_section' => $section,
|
'success_section' => $section,
|
||||||
]);
|
]);
|
||||||
@@ -29,7 +31,7 @@ class BantuanKelompokTaniController extends Controller
|
|||||||
|
|
||||||
private function redirectToSectionWithError(string $url, string $section, string $message)
|
private function redirectToSectionWithError(string $url, string $section, string $message)
|
||||||
{
|
{
|
||||||
return redirect($url . '#section-' . $section)->with([
|
return redirect($url . '#kelompok-tani-notifications')->with([
|
||||||
'error' => $message,
|
'error' => $message,
|
||||||
'error_section' => $section,
|
'error_section' => $section,
|
||||||
]);
|
]);
|
||||||
@@ -39,9 +41,72 @@ class BantuanKelompokTaniController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorizeAccess();
|
$this->authorizeAccess();
|
||||||
|
|
||||||
$kelompokTani->load('bantuan');
|
return redirect($this->operasionalUrl($kelompokTani) . '#section-status-bantuan');
|
||||||
|
}
|
||||||
|
|
||||||
return view('produktivitas.survey-kelompok-tani.bantuan-penilaian', compact('kelompokTani'));
|
private function operasionalUrl(KelompokTani $kelompokTani): string
|
||||||
|
{
|
||||||
|
return route('kelompok-tani.edit', $kelompokTani->id) . '?section=aktivitas-fungsi';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasRecordedAid(KelompokTani $kelompokTani): bool
|
||||||
|
{
|
||||||
|
return $kelompokTani->bantuan()->whereRaw("TRIM(nama_program) <> ''")->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bantuanRules(bool $pernah = true, bool $requireFirstRow = false): array
|
||||||
|
{
|
||||||
|
$exclude = Rule::excludeIf(!$pernah);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'array'],
|
||||||
|
'bantuan.*' => [$exclude, 'array'],
|
||||||
|
'bantuan.*.nama_program' => [$exclude, 'required_with:bantuan.*.penyelenggara,bantuan.*.bentuk_bantuan,bantuan.*.tahun', 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.*.penyelenggara' => [$exclude, 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.*.bentuk_bantuan' => [$exclude, 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.*.tahun' => [$exclude, 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
|
||||||
|
'bantuan.0.nama_program' => [$exclude, Rule::requiredIf($requireFirstRow), 'required_with:bantuan.0.penyelenggara,bantuan.0.bentuk_bantuan,bantuan.0.tahun', 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.0.penyelenggara' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.0.bentuk_bantuan' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'string', 'max:255'],
|
||||||
|
'bantuan.0.tahun' => [$exclude, Rule::requiredIf($requireFirstRow), 'nullable', 'integer', 'min:1900', 'max:' . date('Y')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bantuanMessages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'pernah_mendapatkan_bantuan.required' => 'Pengalaman Akses Bantuan/Program wajib diisi.',
|
||||||
|
'pernah_mendapatkan_bantuan.boolean' => 'Pilih Pernah atau Belum Pernah untuk Pengalaman Akses Bantuan/Program.',
|
||||||
|
'bantuan.required' => 'Isi minimal satu data bantuan jika memilih Pernah.',
|
||||||
|
'bantuan.0.nama_program.required' => 'Nama Program/Bantuan pada baris pertama wajib diisi.',
|
||||||
|
'bantuan.0.penyelenggara.required' => 'Penyelenggara pada baris pertama wajib diisi.',
|
||||||
|
'bantuan.0.bentuk_bantuan.required' => 'Bentuk Bantuan pada baris pertama wajib diisi.',
|
||||||
|
'bantuan.0.tahun.required' => 'Tahun pada baris pertama wajib diisi.',
|
||||||
|
'bantuan.*.nama_program.required_with' => 'Nama Program/Bantuan wajib diisi jika baris bantuan diisi.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, KelompokTani $kelompokTani)
|
||||||
|
{
|
||||||
|
$this->authorizeAccess();
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
|
||||||
|
...$this->bantuanRules(
|
||||||
|
$request->boolean('pernah_mendapatkan_bantuan'),
|
||||||
|
$request->boolean('pernah_mendapatkan_bantuan') && !$this->hasRecordedAid($kelompokTani)
|
||||||
|
),
|
||||||
|
], $this->bantuanMessages());
|
||||||
|
|
||||||
|
DB::transaction(function () use ($kelompokTani, $validated) {
|
||||||
|
$kelompokTani->update([
|
||||||
|
'pernah_mendapatkan_bantuan' => $validated['pernah_mendapatkan_bantuan'],
|
||||||
|
'updated_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
$this->createBantuan($kelompokTani, $validated['bantuan'] ?? []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil disimpan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request, KelompokTani $kelompokTani)
|
public function store(Request $request, KelompokTani $kelompokTani)
|
||||||
@@ -50,37 +115,37 @@ class BantuanKelompokTaniController extends Controller
|
|||||||
|
|
||||||
if (!$kelompokTani->pernah_mendapatkan_bantuan) {
|
if (!$kelompokTani->pernah_mendapatkan_bantuan) {
|
||||||
return $this->redirectToSectionWithError(
|
return $this->redirectToSectionWithError(
|
||||||
route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id),
|
$this->operasionalUrl($kelompokTani),
|
||||||
'status-bantuan',
|
'status-bantuan',
|
||||||
'Set status jadi "Pernah" dulu sebelum menambahkan daftar bantuan/program.'
|
'Operasional: pilih jawaban "Pernah" terlebih dahulu sebelum menambahkan data bantuan/program.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate($this->bantuanRules(true, !$this->hasRecordedAid($kelompokTani)), $this->bantuanMessages());
|
||||||
'bantuan' => ['nullable', 'array'],
|
|
||||||
'bantuan.*.nama_program' => ['nullable', 'string', 'max:255'],
|
|
||||||
'bantuan.*.penyelenggara' => ['nullable', 'string', 'max:255'],
|
|
||||||
'bantuan.*.bentuk_bantuan' => ['nullable', 'string', 'max:255'],
|
|
||||||
'bantuan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($validated['bantuan'] ?? [] as $item) {
|
DB::transaction(fn () => $this->createBantuan($kelompokTani, $validated['bantuan'] ?? []));
|
||||||
if (empty($item['nama_program']) && empty($item['penyelenggara']) && empty($item['bentuk_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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$kelompokTani->bantuan()->create([
|
$kelompokTani->bantuan()->create([
|
||||||
'uid' => 'BKT-' . strtoupper(Str::random(10)),
|
'uid' => 'BKT-' . strtoupper(Str::random(10)),
|
||||||
'nama_program' => $item['nama_program'] ?? null,
|
'nama_program' => $namaProgram,
|
||||||
'penyelenggara' => $item['penyelenggara'] ?? null,
|
'penyelenggara' => $item['penyelenggara'] ?? '',
|
||||||
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? null,
|
'bentuk_bantuan' => $item['bentuk_bantuan'] ?? '',
|
||||||
'tahun' => $item['tahun'] ?? null,
|
'tahun' => $item['tahun'] ?? null,
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil ditambahkan.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan)
|
public function destroy(KelompokTani $kelompokTani, BantuanKelompokTani $bantuan)
|
||||||
@@ -89,9 +154,18 @@ class BantuanKelompokTaniController extends Controller
|
|||||||
|
|
||||||
abort_unless($bantuan->survey_kelompok_tani_id === $kelompokTani->id, 404);
|
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->update(['deleted_by' => auth()->id()]);
|
||||||
$bantuan->delete();
|
$bantuan->delete();
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'bantuan', 'Data bantuan berhasil dihapus.');
|
return $this->redirectToSection($this->operasionalUrl($kelompokTani), 'status-bantuan', 'Data Operasional berhasil dihapus.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
use App\Enums\PendidikanPelatihan\PelatihanGAP;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
use App\Models\Pertanian\SurveyPelatihan;
|
use App\Models\Pertanian\SurveyPelatihan;
|
||||||
use App\Models\Pertanian\SurveySertifikasi;
|
use App\Models\Pertanian\SurveySertifikasi;
|
||||||
@@ -15,7 +17,7 @@ class PendidikanPelatihanController extends Controller
|
|||||||
{
|
{
|
||||||
private function authorizeAccess(): void
|
private function authorizeAccess(): void
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,15 +46,22 @@ class PendidikanPelatihanController extends Controller
|
|||||||
$validatedData = $request->validate([
|
$validatedData = $request->validate([
|
||||||
'riwayat_pelatihan' => ['nullable', 'array'],
|
'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.*.penyelenggara' => ['nullable', 'string', 'max:255'],
|
||||||
'riwayat_pelatihan.*.tahun' => ['nullable', 'integer', 'min:1900', 'max:' . date('Y')],
|
'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.integer' => 'Tahun pelatihan harus berupa angka.',
|
||||||
'riwayat_pelatihan.*.tahun.min' => 'Tahun pelatihan tidak valid.',
|
'riwayat_pelatihan.*.tahun.min' => 'Tahun pelatihan tidak valid.',
|
||||||
'riwayat_pelatihan.*.tahun.max' => 'Tahun pelatihan tidak boleh melebihi tahun saat ini.',
|
'riwayat_pelatihan.*.tahun.max' => 'Tahun pelatihan tidak boleh melebihi tahun saat ini.',
|
||||||
], [
|
], [
|
||||||
'riwayat_pelatihan.*.jenis_pelatihan' => 'jenis pelatihan',
|
'riwayat_pelatihan.*.jenis_pelatihan' => 'jenis pelatihan',
|
||||||
|
'riwayat_pelatihan.*.jenis_pelatihan_lainnya' => 'jenis pelatihan lainnya',
|
||||||
'riwayat_pelatihan.*.penyelenggara' => 'penyelenggara',
|
'riwayat_pelatihan.*.penyelenggara' => 'penyelenggara',
|
||||||
'riwayat_pelatihan.*.tahun' => 'tahun pelatihan',
|
'riwayat_pelatihan.*.tahun' => 'tahun pelatihan',
|
||||||
]);
|
]);
|
||||||
@@ -66,10 +75,15 @@ class PendidikanPelatihanController extends Controller
|
|||||||
continue;
|
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([
|
$petani->surveyPelatihan()->create([
|
||||||
'uid' => 'SPL-' . strtoupper(Str::random(10)),
|
'uid' => 'SPL-' . strtoupper(Str::random(10)),
|
||||||
'petani_id' => $petani->id,
|
'petani_id' => $petani->id,
|
||||||
'jenis_pelatihan' => $item['jenis_pelatihan'] ?? null,
|
'jenis_pelatihan' => $jenisPelatihan,
|
||||||
'penyelenggara' => $item['penyelenggara'] ?? null,
|
'penyelenggara' => $item['penyelenggara'] ?? null,
|
||||||
'tahun_pelaksanaan' => $item['tahun'] ?? null,
|
'tahun_pelaksanaan' => $item['tahun'] ?? null,
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class PengurusKelompokTaniController extends Controller
|
|||||||
{
|
{
|
||||||
private function authorizeAccess(): void
|
private function authorizeAccess(): void
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,7 @@ class PengurusKelompokTaniController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorizeAccess();
|
$this->authorizeAccess();
|
||||||
|
|
||||||
$kelompokTani->load('pengurus');
|
return redirect(route('kelompok-tani.edit', $kelompokTani->id) . '#section-ringkasan');
|
||||||
|
|
||||||
return view('produktivitas.survey-kelompok-tani.pengurus-anggota', compact('kelompokTani'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request, KelompokTani $kelompokTani)
|
public function store(Request $request, KelompokTani $kelompokTani)
|
||||||
@@ -64,7 +62,7 @@ class PengurusKelompokTaniController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil ditambahkan.');
|
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil ditambahkan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus)
|
public function destroy(KelompokTani $kelompokTani, PengurusKelompokTani $pengurus)
|
||||||
@@ -76,6 +74,6 @@ class PengurusKelompokTaniController extends Controller
|
|||||||
$pengurus->update(['deleted_by' => auth()->id()]);
|
$pengurus->update(['deleted_by' => auth()->id()]);
|
||||||
$pengurus->delete();
|
$pengurus->delete();
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'pengurus', 'Data pengurus berhasil dihapus.');
|
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'pengurus', 'Data Profil berhasil dihapus.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use Illuminate\Support\Str;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
use App\Models\Wilayah\Provinsi;
|
use App\Models\Wilayah\Provinsi;
|
||||||
|
use App\Models\Wilayah\KabupatenKota;
|
||||||
|
use App\Models\Wilayah\Kecamatan;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
use App\Models\Wilayah\DesaKelurahan;
|
use App\Models\Wilayah\DesaKelurahan;
|
||||||
use App\Models\Pertanian\Komoditas;
|
use App\Models\Pertanian\Komoditas;
|
||||||
@@ -30,11 +32,36 @@ class PetaniController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$provinsi = Provinsi::select('id', 'kode', 'nama')->orderBy('kode', 'asc')->get();
|
$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,
|
||||||
|
};
|
||||||
|
|
||||||
|
$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([
|
$query = Petani::with([
|
||||||
'desaKelurahan:id,nama,kecamatan_id',
|
'desaKelurahan:id,nama,kecamatan_id',
|
||||||
@@ -43,29 +70,128 @@ class PetaniController extends Controller
|
|||||||
'desaKelurahan.kecamatan.kabupatenKota.provinsi:id,nama',
|
'desaKelurahan.kecamatan.kabupatenKota.provinsi:id,nama',
|
||||||
'komoditas:id,nama',
|
'komoditas:id,nama',
|
||||||
'skor:id,petani_id,skor_kesiapan_adopsi',
|
'skor:id,petani_id,skor_kesiapan_adopsi',
|
||||||
|
'creator:id,name,role',
|
||||||
])
|
])
|
||||||
->orderBy('created_at', 'desc');
|
->withSum('lahans as total_luas_lahan', 'luas_lahan');
|
||||||
|
|
||||||
if ($request->search) {
|
$search = $request->filled('search') ? Str::lower($request->search) : null;
|
||||||
$query->where(function ($query) use ($request) {
|
if ($search) {
|
||||||
$query->where('nama', 'like', '%' . $request->search . '%')
|
$query->where(function ($query) use ($search) {
|
||||||
->orWhere('ktp', 'like', '%' . $request->search . '%');
|
$query->whereRaw('lower(nama) like ?', ["%{$search}%"])
|
||||||
|
->orWhereRaw('lower(ktp) like ?', ["%{$search}%"]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth()->user()->role->value === 'fasilitator') {
|
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);
|
$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');
|
$summaryPetaniIds = (clone $query)->pluck('id');
|
||||||
$petani = $query->paginate($request->per_page ?? 10);
|
$petani = $query->paginate($request->per_page ?? 10);
|
||||||
|
|
||||||
$komoditas = Komoditas::select('id', 'nama')->where('is_utama', true)->get();
|
$komoditas = Komoditas::select('id', 'nama')->where('is_utama', true)->get();
|
||||||
$lahanQuery = Lahan::whereIn('petani_id', $summaryPetaniIds)
|
$komoditasOptions = Komoditas::select('id', 'nama')->orderBy('nama')->get();
|
||||||
->whereIn('komoditas_id', $komoditas->pluck('id'));
|
$kabupatenOptions = KabupatenKota::select('id', 'nama')->orderBy('nama')->get();
|
||||||
$lahanIds = (clone $lahanQuery)->pluck('id');
|
$selectedKabupaten = $filters['kabupaten_kota_id']
|
||||||
$totalLuasLahan = (clone $lahanQuery)->sum('luas_lahan');
|
? KabupatenKota::select('id', 'nama')->find($filters['kabupaten_kota_id'])
|
||||||
$totalTanaman = Tanaman::whereIn('lahan_id', $lahanIds)->count();
|
: 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 = "
|
||||||
CASE
|
CASE
|
||||||
@@ -81,12 +207,30 @@ class PetaniController extends Controller
|
|||||||
->groupByRaw($case)
|
->groupByRaw($case)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('produktivitas.petani.index', compact('petani', 'provinsi', 'komoditas', 'skor', 'totalLuasLahan', 'totalTanaman'));
|
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()
|
public function create()
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +302,7 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +334,7 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function show(string $id)
|
public function show(string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +346,7 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function edit(string $id, Request $request)
|
public function edit(string $id, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +367,7 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $id)
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,14 +409,14 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function destroy(string $id)
|
public function destroy(string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
|
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$petani = Petani::findOrFail($id);
|
$petani = Petani::findOrFail($id);
|
||||||
|
|
||||||
if (auth()->user()->role->value === 'fasilitator' && $petani->created_by !== auth()->id()) {
|
if (auth()->user()->role->value === 'enumerator' && $petani->created_by !== auth()->id()) {
|
||||||
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
|
return response()->json(['message' => 'Anda tidak memiliki akses.'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +443,7 @@ class PetaniController extends Controller
|
|||||||
|
|
||||||
public function showImage(string $id, string $field)
|
public function showImage(string $id, string $field)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class PetaniLahanController extends Controller
|
|||||||
{
|
{
|
||||||
public function index($petaniId, Request $request)
|
public function index($petaniId, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function create($petaniId)
|
public function create($petaniId)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function store($petaniId, Request $request)
|
public function store($petaniId, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function show(string $petaniId, string $id)
|
public function show(string $petaniId, string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function edit(string $petaniId, string $id)
|
public function edit(string $petaniId, string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $petaniId, string $id)
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function destroy(string $petaniId, string $id)
|
public function destroy(string $petaniId, string $id)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +481,7 @@ class PetaniLahanController extends Controller
|
|||||||
|
|
||||||
public function summary(string $petaniId)
|
public function summary(string $petaniId)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class PetaniLahanTanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(string $petaniId, string $lahanId, string $id)
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class PetaniLahanTanamanController extends Controller
|
|||||||
|
|
||||||
public function update(Request $request, string $petaniId, string $lahanId, string $id)
|
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);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class PetaniLahanTanamanController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy(string $petaniId, string $lahanId, string $id)
|
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);
|
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)
|
public function index($petaniId, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ class PraktikBudidayaController extends Controller
|
|||||||
|
|
||||||
public function store($petaniId, Request $request)
|
public function store($petaniId, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +184,12 @@ class PraktikBudidayaController extends Controller
|
|||||||
|
|
||||||
$petani = Petani::findOrFail($petaniId);
|
$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;
|
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
|
||||||
|
|
||||||
$praktikBudidaya = $petani->praktikBudidaya()->create([
|
$praktikBudidaya = $petani->praktikBudidaya()->create([
|
||||||
@@ -241,7 +247,7 @@ class PraktikBudidayaController extends Controller
|
|||||||
|
|
||||||
public function edit($petaniId, $praktikBudidayaId)
|
public function edit($petaniId, $praktikBudidayaId)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@ class PraktikBudidayaController extends Controller
|
|||||||
|
|
||||||
public function update($petaniId, $praktikBudidayaId, Request $request)
|
public function update($petaniId, $praktikBudidayaId, Request $request)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +573,12 @@ class PraktikBudidayaController extends Controller
|
|||||||
|
|
||||||
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
|
$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 = [
|
$dataPraktikBudidaya = [
|
||||||
'tenaga_kerja' => $validatedData['tenaga_kerja'],
|
'tenaga_kerja' => $validatedData['tenaga_kerja'],
|
||||||
'frekuensi_pemangkasan_utama' => $validatedData['frekuensi_pemangkasan_komoditas_utama'],
|
'frekuensi_pemangkasan_utama' => $validatedData['frekuensi_pemangkasan_komoditas_utama'],
|
||||||
@@ -682,7 +694,7 @@ class PraktikBudidayaController extends Controller
|
|||||||
'uid' => 'SP-' . strtoupper(Str::random(10)),
|
'uid' => 'SP-' . strtoupper(Str::random(10)),
|
||||||
'petani_id' => $petani->id,
|
'petani_id' => $petani->id,
|
||||||
'created_by' => auth()->id(),
|
'created_by' => auth()->id(),
|
||||||
'updated_by_by' => auth()->id()
|
'updated_by' => auth()->id()
|
||||||
], $dataSurveyPanen));
|
], $dataSurveyPanen));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,20 +49,43 @@ class ProfilPetaniController extends Controller
|
|||||||
$skorPetani = SkorPetani::where('petani_id', $id)->first();
|
$skorPetani = SkorPetani::where('petani_id', $id)->first();
|
||||||
$enumerator = Profile::where('user_id', $petani->created_by)->first();
|
$enumerator = Profile::where('user_id', $petani->created_by)->first();
|
||||||
$komoditasUtama = Komoditas::where('id', $petani->komoditas_id)->first();
|
$komoditasUtama = Komoditas::where('id', $petani->komoditas_id)->first();
|
||||||
$lahan = Lahan::where('petani_id', $id )->get();
|
$lahan = Lahan::query()
|
||||||
|
->select(['id', 'petani_id', 'nama', 'path', 'luas_lahan', 'komoditas_id', 'desa_kelurahan_id'])
|
||||||
|
->with([
|
||||||
|
'komoditas:id,nama',
|
||||||
|
'desaKelurahan:id,nama,path,kecamatan_id',
|
||||||
|
'desaKelurahan.kecamatan:id,nama,path',
|
||||||
|
])
|
||||||
|
->where('petani_id', $id)
|
||||||
|
->get();
|
||||||
|
|
||||||
$desaKelurahan = DesaKelurahan::with('kecamatan')
|
$desaKelurahan = DesaKelurahan::with('kecamatan')
|
||||||
->where('id', $petani->desa_kelurahan_id)
|
->where('id', $petani->desa_kelurahan_id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$ktpMasked = $petani->ktp ? Str::mask($petani->ktp, '*', 4, -4) : '-';
|
$ktpMasked = $petani->ktp ? Str::mask($petani->ktp, '*', 4, -4) : '-';
|
||||||
$profiling = $skorPetani ? $this->buildProfiling($skorPetani) : null;
|
$assessmentFields = [
|
||||||
|
'skor_kesiapan_adopsi',
|
||||||
|
'skor_d2_sistem_praktik_budidaya',
|
||||||
|
'skor_d4_sosial_ekonomi',
|
||||||
|
'skor_d6_rantai_nilai_akses_pasar',
|
||||||
|
'skor_komp_1_d4_k3_motivasi_kapasitas_adopsi',
|
||||||
|
'skor_komp_2_d4_k2_ekonomi_rumah_tangga',
|
||||||
|
'skor_komp_3_d2_praktik_budidaya_gap',
|
||||||
|
'skor_komp_4_d4_k4_keterlibatan_kelembagaan',
|
||||||
|
'skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan',
|
||||||
|
'skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar',
|
||||||
|
];
|
||||||
|
$hasAssessment = $skorPetani !== null && collect($assessmentFields)->every(
|
||||||
|
fn (string $field) => $skorPetani->{$field} !== null
|
||||||
|
);
|
||||||
|
$profiling = $hasAssessment ? $this->buildProfiling($skorPetani) : null;
|
||||||
|
|
||||||
$tanaman = Tanaman::where('petani_id', $petani->id)
|
$tanaman = Tanaman::where('petani_id', $petani->id)
|
||||||
->where('komoditas_id', $petani->komoditas_id)
|
->where('komoditas_id', $petani->komoditas_id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($tanaman) {
|
if ($tanaman?->updated_at && $tanaman->umur_tanaman !== null) {
|
||||||
$tahunInputTanaman = $tanaman->updated_at->year;
|
$tahunInputTanaman = $tanaman->updated_at->year;
|
||||||
$umurTanaman = (int) $tanaman->umur_tanaman;
|
$umurTanaman = (int) $tanaman->umur_tanaman;
|
||||||
$tahunSekarang = now()->year;
|
$tahunSekarang = now()->year;
|
||||||
@@ -162,8 +185,8 @@ class ProfilPetaniController extends Controller
|
|||||||
|
|
||||||
$d4SosialEkonomi = null;
|
$d4SosialEkonomi = null;
|
||||||
|
|
||||||
$terdaftarKelompok = $sosialEkonomi->terdaftar_kelompok_tani;
|
$terdaftarKelompok = $sosialEkonomi?->terdaftar_kelompok_tani;
|
||||||
$peranKelompok = $sosialEkonomi->peran_dalam_kelompok;
|
$peranKelompok = $sosialEkonomi?->peran_dalam_kelompok;
|
||||||
|
|
||||||
if ($sosialEkonomi) {
|
if ($sosialEkonomi) {
|
||||||
|
|
||||||
@@ -207,8 +230,8 @@ class ProfilPetaniController extends Controller
|
|||||||
'icon' => 'tabler-minus',
|
'icon' => 'tabler-minus',
|
||||||
],
|
],
|
||||||
default => [
|
default => [
|
||||||
'label' => Str::headline($status),
|
'label' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||||
'short' => Str::headline($status),
|
'short' => filled($status) ? Str::headline($status) : 'Belum diisi',
|
||||||
'color' => 'secondary',
|
'color' => 'secondary',
|
||||||
'icon' => 'tabler-help-circle',
|
'icon' => 'tabler-help-circle',
|
||||||
],
|
],
|
||||||
@@ -434,12 +457,61 @@ class ProfilPetaniController extends Controller
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Report missing profile data without treating an unanswered field as zero.
|
||||||
|
$missingSections = [];
|
||||||
|
$identityFields = [
|
||||||
|
'nama' => 'Nama', 'ktp' => 'NIK', 'tanggal_lahir' => 'Tanggal lahir',
|
||||||
|
'jenis_kelamin' => 'Jenis kelamin', 'pendidikan_terakhir' => 'Pendidikan terakhir',
|
||||||
|
'status_perkawinan' => 'Status perkawinan',
|
||||||
|
];
|
||||||
|
foreach ($identityFields as $field => $label) {
|
||||||
|
if (blank($petani->{$field})) {
|
||||||
|
$missingSections[] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$desaKelurahan?->nama || !$desaKelurahan?->kecamatan?->nama) {
|
||||||
|
$missingSections[] = 'Wilayah desa/kecamatan';
|
||||||
|
}
|
||||||
|
if (!$komoditasUtama?->nama) {
|
||||||
|
$missingSections[] = 'Komoditas utama';
|
||||||
|
}
|
||||||
|
if ($lahan->isEmpty()) {
|
||||||
|
$missingSections[] = 'Data lahan';
|
||||||
|
}
|
||||||
|
if ($tahunTanam === null) {
|
||||||
|
$missingSections[] = 'Umur tanaman';
|
||||||
|
}
|
||||||
|
if (!$praktikBudidaya || $indikatorOnFarm->contains(fn ($item) => $item['skor'] === null)) {
|
||||||
|
$missingSections[] = 'Praktik budidaya';
|
||||||
|
}
|
||||||
|
if ($surveyPanen?->hasil_komoditas_utama_permusim === null) {
|
||||||
|
$missingSections[] = 'Survei panen';
|
||||||
|
}
|
||||||
|
$socialFields = [
|
||||||
|
'jumlah_tanggungan', 'skor_kecukupan_penghasilan', 'skor_kemampuan_menabung',
|
||||||
|
'status_kepemilikan_lahan', 'terdaftar_kelompok_tani', 'status_rekening_tabungan',
|
||||||
|
'pengalaman_kredit_kebun', 'keterikatan_hutang_panen', 'praktik_ijon',
|
||||||
|
'kebebasan_memilih_pembeli',
|
||||||
|
];
|
||||||
|
if (!$sosialEkonomi || collect($socialFields)->contains(fn ($field) => blank($sosialEkonomi->{$field}))) {
|
||||||
|
$missingSections[] = 'Sosial ekonomi';
|
||||||
|
}
|
||||||
|
if (!$aksesPasar || blank($aksesPasar->saluran_terdahulu)) {
|
||||||
|
$missingSections[] = 'Akses pasar';
|
||||||
|
}
|
||||||
|
if (!$hasAssessment) {
|
||||||
|
$missingSections[] = 'Penilaian kesiapan adopsi belum lengkap';
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
|
'missingSections' => $missingSections,
|
||||||
|
'hasAssessment' => $hasAssessment,
|
||||||
'petani' => $petani,
|
'petani' => $petani,
|
||||||
'skorPetani' => $skorPetani,
|
'skorPetani' => $skorPetani,
|
||||||
'enumerator' => $enumerator,
|
'enumerator' => $enumerator,
|
||||||
'komoditasUtama' => $komoditasUtama,
|
'komoditasUtama' => $komoditasUtama,
|
||||||
'lahan' => $lahan,
|
'lahan' => $lahan,
|
||||||
|
'farmerLandMap' => $this->farmerLandMapData($lahan),
|
||||||
'desaKelurahan' => $desaKelurahan,
|
'desaKelurahan' => $desaKelurahan,
|
||||||
'ktpMasked' => $ktpMasked,
|
'ktpMasked' => $ktpMasked,
|
||||||
'profiling' => $profiling,
|
'profiling' => $profiling,
|
||||||
@@ -447,7 +519,6 @@ class ProfilPetaniController extends Controller
|
|||||||
'umurTanaman' => $umurTanaman,
|
'umurTanaman' => $umurTanaman,
|
||||||
'surveyPanen' => $surveyPanen,
|
'surveyPanen' => $surveyPanen,
|
||||||
'praktikBudidaya' => $praktikBudidaya,
|
'praktikBudidaya' => $praktikBudidaya,
|
||||||
'praktikBudidaya' => $praktikBudidaya,
|
|
||||||
'indikatorOnFarm' => $indikatorOnFarm,
|
'indikatorOnFarm' => $indikatorOnFarm,
|
||||||
'detailOnFarm' => $detailOnFarm,
|
'detailOnFarm' => $detailOnFarm,
|
||||||
'sosialEkonomi' => $sosialEkonomi,
|
'sosialEkonomi' => $sosialEkonomi,
|
||||||
@@ -464,6 +535,35 @@ class ProfilPetaniController extends Controller
|
|||||||
return view('produktivitas.profil-petani.index', $data);
|
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
|
private function getScoreColor(?int $score): string
|
||||||
{
|
{
|
||||||
if ($score === null) {
|
if ($score === null) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SkorPetaniController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(string $petaniId)
|
public function index(string $petaniId)
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'koordinator', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,23 +6,27 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
use App\Models\Pertanian\KelompokTani;
|
use App\Models\Pertanian\KelompokTani;
|
||||||
use App\Models\Wilayah\Provinsi;
|
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;
|
use App\Models\Pertanian\Komoditas;
|
||||||
|
|
||||||
class SurveyKelompokTaniController extends Controller
|
class SurveyKelompokTaniController extends Controller
|
||||||
{
|
{
|
||||||
private function authorizeAccess(): void
|
private function authorizeAccess(): void
|
||||||
{
|
{
|
||||||
if (!Gate::any(['super_admin', 'admin', 'fasilitator'])) {
|
if (!Gate::any(['super_admin', 'admin', 'enumerator'])) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function redirectToSection(string $url, string $section, string $message)
|
private function redirectToSection(string $url, string $section, string $message, ?string $nextSection = null)
|
||||||
{
|
{
|
||||||
return redirect($url . '#section-' . $section)->with([
|
return redirect($url . '#section-' . ($nextSection ?? $section))->with([
|
||||||
'success' => $message,
|
'success' => $message,
|
||||||
'success_section' => $section,
|
'success_section' => $section,
|
||||||
]);
|
]);
|
||||||
@@ -30,13 +34,90 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$kelompokTani = KelompokTani::query()
|
$validated = $request->validate([
|
||||||
->when($request->search, fn ($query, $search) => $query->where('nama_kelompok', 'like', "%{$search}%"))
|
'search' => ['nullable', 'string', 'max:255'],
|
||||||
->latest()
|
'kabupaten_kota_id' => ['nullable', 'integer', 'exists:master_kabupaten_kota,id'],
|
||||||
->paginate($request->per_page ?? 10)
|
'kecamatan_id' => ['nullable', 'integer', 'exists:master_kecamatan,id'],
|
||||||
->appends($request->query());
|
'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'],
|
||||||
|
]);
|
||||||
|
|
||||||
return view('produktivitas.survey-kelompok-tani.index', compact('kelompokTani'));
|
$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()
|
public function create()
|
||||||
@@ -46,7 +127,24 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
$provinsi = Provinsi::orderBy('kode', 'asc')->get();
|
||||||
$komoditas = Komoditas::where('is_utama', true)->get();
|
$komoditas = Komoditas::where('is_utama', true)->get();
|
||||||
|
|
||||||
return view('produktivitas.survey-kelompok-tani.add', compact('provinsi', 'komoditas'));
|
return view('produktivitas.survey-kelompok-tani.add', [
|
||||||
|
...compact('provinsi', 'komoditas'),
|
||||||
|
...$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)
|
public function store(Request $request)
|
||||||
@@ -67,7 +165,7 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil ditambahkan.');
|
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil ditambahkan.', 'pengurus');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string
|
private function editUrl(KelompokTani $kelompokTani, string $section = 'identitas'): string
|
||||||
@@ -83,12 +181,15 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
|
|
||||||
return match ($request->query('section', 'identitas')) {
|
return match ($request->query('section', 'identitas')) {
|
||||||
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')),
|
'kelembagaan' => view('produktivitas.survey-kelompok-tani.kelembagaan', compact('kelompokTani')),
|
||||||
'aktivitas-fungsi' => view('produktivitas.survey-kelompok-tani.aktivitas-fungsi', 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')),
|
'sosial-konflik' => view('produktivitas.survey-kelompok-tani.sosial-konflik', compact('kelompokTani')),
|
||||||
default => view('produktivitas.survey-kelompok-tani.edit', [
|
default => view('produktivitas.survey-kelompok-tani.edit', [
|
||||||
'kelompokTani' => $kelompokTani,
|
'kelompokTani' => $kelompokTani->loadMissing('pengurus'),
|
||||||
'provinsi' => Provinsi::orderBy('kode', 'asc')->get(),
|
'provinsi' => Provinsi::orderBy('kode', 'asc')->get(),
|
||||||
'komoditas' => Komoditas::where('is_utama', true)->get(),
|
'komoditas' => Komoditas::where('is_utama', true)->get(),
|
||||||
|
...$this->profileLocationOptions($kelompokTani),
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,8 +204,8 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani),
|
'aktivitas-fungsi' => $this->updateAktivitasFungsi($request, $kelompokTani),
|
||||||
'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani),
|
'kas' => $this->updateKasSimpanPinjam($request, $kelompokTani),
|
||||||
'sosial-konflik' => $this->updateSosialKonflik($request, $kelompokTani),
|
'sosial-konflik' => $this->updateSosialKonflik($request, $kelompokTani),
|
||||||
'status-bantuan' => $this->updateStatusBantuan($request, $kelompokTani),
|
'status-bantuan' => app(BantuanKelompokTaniController::class)->update($request, $kelompokTani),
|
||||||
'penilaian' => $this->updatePenilaian($request, $kelompokTani),
|
'penilaian' => redirect($this->editUrl($kelompokTani, 'aktivitas-fungsi') . '#section-status-bantuan'),
|
||||||
default => $this->updateIdentitas($request, $kelompokTani),
|
default => $this->updateIdentitas($request, $kelompokTani),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -122,7 +223,7 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
|
|
||||||
private function validateIdentitas(Request $request)
|
private function validateIdentitas(Request $request)
|
||||||
{
|
{
|
||||||
return $request->validate([
|
$validated = $request->validate([
|
||||||
// Data Responden
|
// Data Responden
|
||||||
'nama_responden' => ['required', 'string', 'max:255'],
|
'nama_responden' => ['required', 'string', 'max:255'],
|
||||||
'jabatan_responden' => ['nullable', 'string', 'max:255'],
|
'jabatan_responden' => ['nullable', 'string', 'max:255'],
|
||||||
@@ -130,7 +231,10 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
|
|
||||||
// Identitas Kelompok Tani
|
// Identitas Kelompok Tani
|
||||||
'nama_kelompok' => ['required', 'string', 'max:255'],
|
'nama_kelompok' => ['required', 'string', 'max:255'],
|
||||||
'desa_kelurahan_id' => ['required', 'exists:master_desa_kelurahan,id'],
|
'provinsi_id' => ['bail', 'required', 'integer', 'exists:master_provinsi,id'],
|
||||||
|
'kabupaten_id' => ['bail', 'required', 'integer', 'exists:master_kabupaten_kota,id'],
|
||||||
|
'kecamatan_id' => ['bail', 'required', 'integer', 'exists:master_kecamatan,id'],
|
||||||
|
'desa_kelurahan_id' => ['bail', 'required', 'integer', 'exists:master_desa_kelurahan,id'],
|
||||||
'dusun' => ['nullable', 'string', 'max:255'],
|
'dusun' => ['nullable', 'string', 'max:255'],
|
||||||
'tahun_berdiri' => ['nullable', 'digits:4', 'integer', 'min:1900', 'max:' . date('Y')],
|
'tahun_berdiri' => ['nullable', 'digits:4', 'integer', 'min:1900', 'max:' . date('Y')],
|
||||||
'jenis_kelompok' => ['required', 'string', 'max:50'],
|
'jenis_kelompok' => ['required', 'string', 'max:50'],
|
||||||
@@ -138,15 +242,30 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'nama_gapoktan' => ['nullable', 'string', 'max:255'],
|
'nama_gapoktan' => ['nullable', 'string', 'max:255'],
|
||||||
'komoditas_id' => ['nullable', 'array'],
|
'komoditas_id' => ['nullable', 'array'],
|
||||||
'komoditas_id.*' => ['exists:master_komoditas,id'],
|
'komoditas_id.*' => ['exists:master_komoditas,id'],
|
||||||
|
...$this->keanggotaanRules(),
|
||||||
], [
|
], [
|
||||||
'nama_responden.required' => 'Nama responden wajib diisi.',
|
'nama_responden.required' => 'Nama responden wajib diisi.',
|
||||||
'nama_kelompok.required' => 'Nama kelompok 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.required' => 'Desa/Kelurahan wajib dipilih.',
|
||||||
'desa_kelurahan_id.exists' => 'Desa/Kelurahan tidak ditemukan.',
|
'desa_kelurahan_id.exists' => 'Desa/Kelurahan tidak ditemukan.',
|
||||||
'jenis_kelompok.required' => 'Jenis kelompok wajib dipilih.',
|
'jenis_kelompok.required' => 'Terdaftar/Dikenal Sebagai wajib diisi.',
|
||||||
'jenis_kelompok_lainnya.required_if' => 'Sebutkan jenis kelompok lainnya.',
|
'jenis_kelompok_lainnya.required_if' => 'Sebutkan Jenis Kelompok wajib diisi.',
|
||||||
'tahun_berdiri.digits' => 'Tahun berdiri harus 4 digit.',
|
'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)
|
private function updateIdentitas(Request $request, KelompokTani $kelompokTani)
|
||||||
@@ -163,12 +282,12 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data kelompok tani berhasil diperbarui.');
|
return $this->redirectToSection(route('kelompok-tani.edit', $kelompokTani->id), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateRingkasanPengurus(Request $request, KelompokTani $kelompokTani)
|
private function keanggotaanRules(): array
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
return [
|
||||||
'jumlah_pengurus' => ['nullable', 'integer', 'min:0'],
|
'jumlah_pengurus' => ['nullable', 'integer', 'min:0'],
|
||||||
'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'],
|
'jumlah_pengurus_aktif' => ['nullable', 'integer', 'min:0'],
|
||||||
'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'],
|
'jumlah_anggota_terdaftar' => ['nullable', 'integer', 'min:0'],
|
||||||
@@ -179,18 +298,24 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'jumlah_anggota_perempuan' => ['nullable', 'integer', 'min:0'],
|
'jumlah_anggota_perempuan' => ['nullable', 'integer', 'min:0'],
|
||||||
'jumlah_anggota_pemuda_laki_laki' => ['nullable', 'integer', 'min:0'],
|
'jumlah_anggota_pemuda_laki_laki' => ['nullable', 'integer', 'min:0'],
|
||||||
'jumlah_anggota_pemuda_perempuan' => ['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([
|
$kelompokTani->update([
|
||||||
...$validated,
|
...$validated,
|
||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.pengurus-anggota.index', $kelompokTani->id), 'ringkasan', 'Ringkasan pengurus & anggota berhasil disimpan.');
|
return $this->redirectToSection($this->editUrl($kelompokTani), 'identitas', 'Data Profil berhasil disimpan.', 'pengurus');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateKelembagaan(Request $request, KelompokTani $kelompokTani)
|
private function updateKelembagaan(Request $request, KelompokTani $kelompokTani)
|
||||||
{
|
{
|
||||||
|
$hasSk = in_array($request->input('status_legalitas'), KelompokTani::STATUS_LEGALITAS_DENGAN_SK, true);
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
// Cakupan Wilayah Kelompok
|
// Cakupan Wilayah Kelompok
|
||||||
'cakupan_wilayah' => ['nullable', 'string', 'max:50'],
|
'cakupan_wilayah' => ['nullable', 'string', 'max:50'],
|
||||||
@@ -198,17 +323,9 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
|
|
||||||
// Status Legalitas Kelompok
|
// Status Legalitas Kelompok
|
||||||
'status_legalitas' => ['nullable', 'string', 'max:50'],
|
'status_legalitas' => ['nullable', 'string', 'max:50'],
|
||||||
'nomor_sk' => ['nullable', 'string', 'max:100'],
|
'nomor_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:100'],
|
||||||
'tanggal_tahun_penerbitan_sk' => ['nullable', 'string', 'max:30'],
|
'tanggal_tahun_penerbitan_sk' => [Rule::excludeIf(!$hasSk), 'nullable', 'string', 'max:30'],
|
||||||
|
|
||||||
// Kualitas Kepemimpinan
|
|
||||||
'tingkat_keaktifan_ketua' => ['nullable', 'string', 'max:30'],
|
|
||||||
'status_pembagian_peran' => ['nullable', 'string', 'max:30'],
|
|
||||||
'status_dokumentasi_rapat' => ['nullable', 'string', 'max:30'],
|
|
||||||
|
|
||||||
// Frekuensi Pertemuan Rutin
|
|
||||||
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
|
|
||||||
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$kelompokTani->update([
|
$kelompokTani->update([
|
||||||
@@ -216,7 +333,7 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data kelembagaan berhasil disimpan.');
|
return $this->redirectToSection($this->editUrl($kelompokTani, 'kelembagaan'), 'kelembagaan', 'Data Kelembagaan berhasil disimpan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array
|
private function resolveLainnya(?array $selected, ?string $lainnyaText): ?array
|
||||||
@@ -233,6 +350,10 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
private function updateAktivitasFungsi(Request $request, KelompokTani $kelompokTani)
|
private function updateAktivitasFungsi(Request $request, KelompokTani $kelompokTani)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
// Frekuensi Pertemuan Rutin
|
||||||
|
'frekuensi_pertemuan' => ['nullable', 'string', 'max:50'],
|
||||||
|
'tanggal_pertemuan_terakhir' => ['nullable', 'date'],
|
||||||
|
|
||||||
// Kegiatan Konkret yang Sudah Berjalan
|
// Kegiatan Konkret yang Sudah Berjalan
|
||||||
'kegiatan_terlaksana' => ['nullable', 'array'],
|
'kegiatan_terlaksana' => ['nullable', 'array'],
|
||||||
'kegiatan_terlaksana.*' => ['string', 'max:255'],
|
'kegiatan_terlaksana.*' => ['string', 'max:255'],
|
||||||
@@ -270,7 +391,7 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data aktivitas & fungsi kelompok berhasil disimpan.');
|
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'aktivitas-fungsi', 'Data Operasional berhasil disimpan.', 'aset-baku');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani)
|
private function updateKasSimpanPinjam(Request $request, KelompokTani $kelompokTani)
|
||||||
@@ -287,22 +408,28 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.aset-keuangan.index', $kelompokTani->id), 'kas', 'Data kas/simpan-pinjam berhasil disimpan.');
|
return $this->redirectToSection($this->editUrl($kelompokTani, 'aktivitas-fungsi'), 'kas', 'Data Operasional berhasil disimpan.', 'status-bantuan');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateSosialKonflik(Request $request, KelompokTani $kelompokTani)
|
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([
|
$validated = $request->validate([
|
||||||
// Keterlibatan Perempuan
|
// Keterlibatan Perempuan
|
||||||
'partisipasi_perempuan' => ['nullable', 'string', 'max:255'],
|
'partisipasi_perempuan' => ['nullable', 'string', 'max:255'],
|
||||||
|
|
||||||
// Riwayat Konflik Internal Kelompok
|
// Riwayat Konflik Internal Kelompok
|
||||||
'konflik_kelompok' => ['nullable', 'string', 'max:255'],
|
'konflik_kelompok' => ['nullable', 'string', 'max:255'],
|
||||||
'topik_konflik' => ['nullable', 'string', 'max:255'],
|
'topik_konflik' => [Rule::requiredIf($pernahKonflik), 'nullable', 'string', 'max:255'],
|
||||||
'penyelesaian_konflik' => ['nullable', 'string'],
|
'penyelesaian_konflik' => ['nullable', 'string'],
|
||||||
|
], [
|
||||||
|
'topik_konflik.required' => 'Jika Pernah, Soal Apa? wajib diisi jika pernah terjadi konflik internal.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!in_array($validated['konflik_kelompok'] ?? null, ['Pernah, sudah selesai', 'Pernah, masih berlanjut/belum selesai'])) {
|
if (!$pernahKonflik) {
|
||||||
$validated['topik_konflik'] = null;
|
$validated['topik_konflik'] = null;
|
||||||
$validated['penyelesaian_konflik'] = null;
|
$validated['penyelesaian_konflik'] = null;
|
||||||
}
|
}
|
||||||
@@ -312,35 +439,7 @@ class SurveyKelompokTaniController extends Controller
|
|||||||
'updated_by' => auth()->id(),
|
'updated_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data sosial & konflik berhasil disimpan.');
|
return $this->redirectToSection($this->editUrl($kelompokTani, 'sosial-konflik'), 'sosial-konflik', 'Data Sosial berhasil disimpan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateStatusBantuan(Request $request, KelompokTani $kelompokTani)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'pernah_mendapatkan_bantuan' => ['required', 'boolean'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$kelompokTani->update([
|
|
||||||
...$validated,
|
|
||||||
'updated_by' => auth()->id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'status-bantuan', 'Status akses bantuan berhasil disimpan.');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updatePenilaian(Request $request, KelompokTani $kelompokTani)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'potensi_kelompok' => ['nullable', 'string', 'max:50'],
|
|
||||||
'alasan_penilaian_potensi' => ['nullable', 'string'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$kelompokTani->update([
|
|
||||||
...$validated,
|
|
||||||
'updated_by' => auth()->id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->redirectToSection(route('kelompok-tani.bantuan-penilaian.index', $kelompokTani->id), 'penilaian', 'Penilaian potensi kelompok berhasil disimpan.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ class SurveyProduktivitasController extends Controller
|
|||||||
{
|
{
|
||||||
$petani = Petani::findOrFail($petaniId);
|
$petani = Petani::findOrFail($petaniId);
|
||||||
|
|
||||||
|
// Baris yang benar-benar kosong dilewati (sama seperti Pendidikan & Pelatihan)
|
||||||
|
$request->merge([
|
||||||
|
'produktivitas_komoditas' => collect($request->input('produktivitas_komoditas', []))
|
||||||
|
->filter(fn ($row) => filled($row['komoditas_id'] ?? null)
|
||||||
|
|| filled($row['hasil_panen_3_musim'] ?? null)
|
||||||
|
|| filled($row['satuan'] ?? null))
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
]);
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'produktivitas_komoditas' => ['required', 'array', 'min:1'],
|
'produktivitas_komoditas' => ['required', 'array', 'min:1'],
|
||||||
'produktivitas_komoditas.*.komoditas_id' => ['required', 'exists:master_komoditas,id'],
|
'produktivitas_komoditas.*.komoditas_id' => ['required', 'exists:master_komoditas,id'],
|
||||||
|
|||||||
@@ -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,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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -15,17 +16,25 @@ use App\Enums\AksesPasar\SkorKesadaranPremium;
|
|||||||
|
|
||||||
class AksesPasar extends Model
|
class AksesPasar extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'akses_pasar';
|
protected $table = 'akses_pasar';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'uid',
|
'uid',
|
||||||
'petani_id',
|
'petani_id',
|
||||||
|
'tujuan_penjualan',
|
||||||
|
'tujuan_penjualan_lainnya',
|
||||||
|
'sifat_hubungan_bisnis',
|
||||||
|
'bentuk_penjualan',
|
||||||
|
'frekuensi_penjualan',
|
||||||
'skor_pengalaman',
|
'skor_pengalaman',
|
||||||
'saluran_terdahulu',
|
'saluran_terdahulu',
|
||||||
'saluran_terdahulu_lainnya',
|
'saluran_terdahulu_lainnya',
|
||||||
'alasan_berhenti',
|
'alasan_berhenti',
|
||||||
|
'tahu_harga_sebelumnya',
|
||||||
|
'sumber_info_harga',
|
||||||
|
'sumber_info_harga_lainnya',
|
||||||
'praktik_fermentasi',
|
'praktik_fermentasi',
|
||||||
'proses_fermentasi',
|
'proses_fermentasi',
|
||||||
'model_fermentasi_memungkinkan',
|
'model_fermentasi_memungkinkan',
|
||||||
@@ -44,7 +53,9 @@ class AksesPasar extends Model
|
|||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'tujuan_penjualan' => 'array',
|
||||||
'saluran_terdahulu' => 'array',
|
'saluran_terdahulu' => 'array',
|
||||||
|
'sumber_info_harga' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
@@ -67,7 +78,7 @@ class AksesPasar extends Model
|
|||||||
$skor->update([
|
$skor->update([
|
||||||
'skor_d6_k1_6_pengalaman_menjual_ke_saluran_berbeda' => $aksesPasar->skor_pengalaman,
|
'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_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).
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
|
|
||||||
class HargaJualPanen extends Model
|
class HargaJualPanen extends Model
|
||||||
{
|
{
|
||||||
|
use KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'harga_jual_panen';
|
protected $table = 'harga_jual_panen';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ class KelompokTani extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
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 $table = 'survey_kelompok_tani';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use App\Models\Wilayah\DesaKelurahan;
|
use App\Models\Wilayah\DesaKelurahan;
|
||||||
@@ -22,7 +23,7 @@ use App\Models\Pertanian\Petani;
|
|||||||
|
|
||||||
class Lahan extends Model
|
class Lahan extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'map_lahan';
|
protected $table = 'map_lahan';
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class PendapatanKomoditas extends Model
|
class PendapatanKomoditas extends Model
|
||||||
{
|
{
|
||||||
|
use KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'pendapatan_komoditas';
|
protected $table = 'pendapatan_komoditas';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
@@ -29,4 +32,11 @@ class PendapatanKomoditas extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(SosialEkonomi::class, 'sosial_ekonomi_id');
|
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;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class PendapatanLuarPertanian extends Model
|
class PendapatanLuarPertanian extends Model
|
||||||
{
|
{
|
||||||
|
use KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'pendapatan_luar_pertanian';
|
protected $table = 'pendapatan_luar_pertanian';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
@@ -27,4 +30,11 @@ class PendapatanLuarPertanian extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(SosialEkonomi::class, 'sosial_ekonomi_id');
|
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,8 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use App\Enums\Pendidikan;
|
use App\Enums\Pendidikan;
|
||||||
use App\Enums\Pernikahan;
|
use App\Enums\Pernikahan;
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Enums\StatusDataPetani;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Wilayah\DesaKelurahan;
|
use App\Models\Wilayah\DesaKelurahan;
|
||||||
use App\Models\Pertanian\Lahan;
|
use App\Models\Pertanian\Lahan;
|
||||||
@@ -28,7 +31,7 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
class Petani extends Model
|
class Petani extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'petani';
|
protected $table = 'petani';
|
||||||
|
|
||||||
@@ -70,6 +73,12 @@ class Petani extends Model
|
|||||||
|
|
||||||
'pendidikan_terakhir' => Pendidikan::class,
|
'pendidikan_terakhir' => Pendidikan::class,
|
||||||
'status_perkawinan' => Pernikahan::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
|
public function getUmurAttribute(): ?int
|
||||||
@@ -113,6 +122,20 @@ class Petani extends Model
|
|||||||
return $this->belongsTo(Komoditas::class, 'komoditas_id');
|
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
|
public function skor(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(SkorPetani::class, 'petani_id');
|
return $this->hasOne(SkorPetani::class, 'petani_id');
|
||||||
@@ -398,4 +421,10 @@ class Petani extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'created_by');
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Untuk trait KembalikanStatusPetaniKeDraf: petani ini sendiri.
|
||||||
|
protected function petaniIdUntukStatus(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -18,7 +19,7 @@ use App\Models\Pertanian\Lahan;
|
|||||||
|
|
||||||
class PraktikBudidaya extends Model
|
class PraktikBudidaya extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'praktik_budidaya';
|
protected $table = 'praktik_budidaya';
|
||||||
|
|
||||||
@@ -101,27 +102,11 @@ class PraktikBudidaya extends Model
|
|||||||
$jenisHama = $praktikBudidaya->jenis_hama ?? [];
|
$jenisHama = $praktikBudidaya->jenis_hama ?? [];
|
||||||
$jenisPenyakit = $praktikBudidaya->jenis_penyakit ?? [];
|
$jenisPenyakit = $praktikBudidaya->jenis_penyakit ?? [];
|
||||||
|
|
||||||
if (Schema::hasColumn((new Lahan)->getTable(), 'jenis_hama')
|
// Salin jenis hama/penyakit ke semua lahan & tanaman petani: satu query per tabel
|
||||||
&& Schema::hasColumn((new Lahan)->getTable(), 'jenis_penyakit')) {
|
// (sebelumnya satu query per baris; petani bisa punya ratusan titik tanaman).
|
||||||
Lahan::where('petani_id', $praktikBudidaya->petani_id)->get()->each(function (Lahan $lahan) use ($jenisHama, $jenisPenyakit, $praktikBudidaya) {
|
// Hanya baris yang isinya berbeda yang diubah, supaya updated_at baris lain tidak ikut berubah.
|
||||||
$lahan->update([
|
self::sinkronHamaPenyakit(new Lahan, $praktikBudidaya, $jenisHama, $jenisPenyakit);
|
||||||
'jenis_hama' => $jenisHama,
|
self::sinkronHamaPenyakit(new Tanaman, $praktikBudidaya, $jenisHama, $jenisPenyakit);
|
||||||
'jenis_penyakit' => $jenisPenyakit,
|
|
||||||
'updated_by' => $praktikBudidaya->updated_by,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasColumn((new Tanaman)->getTable(), 'jenis_hama')
|
|
||||||
&& Schema::hasColumn((new Tanaman)->getTable(), 'jenis_penyakit')) {
|
|
||||||
Tanaman::where('petani_id', $praktikBudidaya->petani_id)->get()->each(function (Tanaman $tanaman) use ($jenisHama, $jenisPenyakit, $praktikBudidaya) {
|
|
||||||
$tanaman->update([
|
|
||||||
'jenis_hama' => $jenisHama,
|
|
||||||
'jenis_penyakit' => $jenisPenyakit,
|
|
||||||
'updated_by' => $praktikBudidaya->updated_by,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($kebiasaan == 0) {
|
if ($kebiasaan == 0) {
|
||||||
$skor_konservasi_tanah = 1;
|
$skor_konservasi_tanah = 1;
|
||||||
@@ -146,6 +131,37 @@ class PraktikBudidaya extends Model
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @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
|
public function petani(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Petani::class, 'petani_id');
|
return $this->belongsTo(Petani::class, 'petani_id');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use App\Enums\SosialEkonomi\AlasanKreditDitolak;
|
use App\Enums\SosialEkonomi\AlasanKreditDitolak;
|
||||||
use App\Enums\SosialEkonomi\BisaBeliPupukBersubsidi;
|
use App\Enums\SosialEkonomi\BisaBeliPupukBersubsidi;
|
||||||
use App\Enums\SosialEkonomi\JenisAsetProduktif;
|
use App\Enums\SosialEkonomi\JenisAsetProduktif;
|
||||||
@@ -51,7 +52,7 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
class SosialEkonomi extends Model
|
class SosialEkonomi extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'sosial_ekonomi';
|
protected $table = 'sosial_ekonomi';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -11,7 +12,7 @@ use App\Models\Pertanian\Petani;
|
|||||||
|
|
||||||
class SurveyPanen extends Model
|
class SurveyPanen extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'survey_panen';
|
protected $table = 'survey_panen';
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ class SurveyPanen extends Model
|
|||||||
$skor->update([
|
$skor->update([
|
||||||
'skor_d2_k3_1_penentuan_pematangan' => $survey->dasar_penentuan_panen,
|
'skor_d2_k3_1_penentuan_pematangan' => $survey->dasar_penentuan_panen,
|
||||||
'skor_d2_k3_5_sortasi_awal_pemisahan_kualitas' => $survey->praktik_sortasi,
|
'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,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
@@ -11,7 +12,7 @@ use App\Models\Pertanian\Petani;
|
|||||||
|
|
||||||
class SurveyPelatihan extends Model
|
class SurveyPelatihan extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'survey_pelatihan';
|
protected $table = 'survey_pelatihan';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
|
|
||||||
class SurveyProduktivitas extends Model
|
class SurveyProduktivitas extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'survey_produktivitas';
|
protected $table = 'survey_produktivitas';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
|
|
||||||
class SurveySertifikasi extends Model
|
class SurveySertifikasi extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'survey_sertifikasi';
|
protected $table = 'survey_sertifikasi';
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models\Pertanian;
|
namespace App\Models\Pertanian;
|
||||||
|
|
||||||
|
use App\Models\Concerns\KembalikanStatusPetaniKeDraf;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use App\Models\Profile;
|
use App\Models\Profile;
|
||||||
@@ -17,7 +18,7 @@ use App\Enums\KeparahanSerangan;
|
|||||||
|
|
||||||
class Tanaman extends Model
|
class Tanaman extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes, KembalikanStatusPetaniKeDraf;
|
||||||
|
|
||||||
protected $table = 'map_tanaman';
|
protected $table = 'map_tanaman';
|
||||||
|
|
||||||
@@ -80,4 +81,11 @@ class Tanaman extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Lahan::class, 'lahan_id');
|
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 Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
|
use App\Models\Konfigurasi\KoordinatorKomoditas;
|
||||||
use App\Models\Pertanian\Petani;
|
use App\Models\Pertanian\Petani;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
@@ -52,4 +53,42 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Petani::class, 'created_by');
|
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;
|
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) {
|
Gate::define('fasilitator', function (User $user) {
|
||||||
return $user->role === Role::FASILITATOR;
|
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 {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->append(\App\Http\Middleware\ResolveProgram::class);
|
$middleware->append(\App\Http\Middleware\ResolveProgram::class);
|
||||||
|
|
||||||
|
$middleware->alias([
|
||||||
|
'akses.petani' => \App\Http\Middleware\BatasiAksesPetani::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->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' => [
|
'postmark' => [
|
||||||
'key' => env('POSTMARK_API_KEY'),
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,479 @@
|
|||||||
|
<?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('riwayat_petani', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan')->comment('Penanda waktu snapshot, misal: Q1 2026');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
|
||||||
|
$table->foreignId('petani_id')->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('nama');
|
||||||
|
$table->boolean('jenis_kelamin');
|
||||||
|
$table->string('tempat_lahir')->nullable();
|
||||||
|
$table->date('tanggal_lahir');
|
||||||
|
$table->string('pendidikan_terakhir')->nullable();
|
||||||
|
|
||||||
|
$table->string('alamat')->nullable();
|
||||||
|
$table->foreignId('desa_kelurahan_id')->nullable()->constrained('master_desa_kelurahan');
|
||||||
|
$table->string('nomor_telepon')->nullable();
|
||||||
|
$table->string('foto', 500)->nullable();
|
||||||
|
$table->string('status_perkawinan')->nullable();
|
||||||
|
|
||||||
|
$table->string('kk')->nullable();
|
||||||
|
$table->string('ktp')->nullable();
|
||||||
|
$table->string('path_kk', 500)->nullable();
|
||||||
|
$table->string('file_kk')->nullable();
|
||||||
|
$table->string('path_ktp', 500)->nullable();
|
||||||
|
$table->string('file_ktp')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_map_lahan', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan')->comment('Penanda waktu snapshot, misal: Q1 2026');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->string('nama');
|
||||||
|
$table->string('status_kepemilikan');
|
||||||
|
|
||||||
|
$table->foreignId('profile_id')->constrained('profiles');
|
||||||
|
$table->foreignId('keluarga_pengelola_id')->nullable()->constrained('keluarga')->onDelete('set null');
|
||||||
|
$table->foreignId('adat_pemilik_id')->nullable()->constrained('adat')->onDelete('set null');
|
||||||
|
$table->foreignId('desa_kelurahan_id')->nullable()->constrained('master_desa_kelurahan');
|
||||||
|
|
||||||
|
$table->string('lat')->nullable();
|
||||||
|
$table->string('long')->nullable();
|
||||||
|
$table->jsonb('path')->nullable();
|
||||||
|
$table->boolean('sertifikat')->default(false);
|
||||||
|
$table->boolean('konflik')->default(false);
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
$table->float('luas_lahan', 8)->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_sertifikat_lahan', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->string('nomor_sertifikat');
|
||||||
|
$table->date('tanggal_terbit');
|
||||||
|
$table->date('tanggal_kadaluarsa')->nullable();
|
||||||
|
$table->string('lembaga_penerbit')->nullable();
|
||||||
|
|
||||||
|
$table->foreignId('lahan_id')->constrained('map_lahan')->onDelete('cascade');
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_map_tanaman', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->string('nama');
|
||||||
|
|
||||||
|
$table->foreignId('komoditas_id')->constrained('master_komoditas')->onDelete('set null');
|
||||||
|
$table->foreignId('jenis_komoditas_id')->constrained('jenis_komoditas')->onDelete('set null');
|
||||||
|
$table->foreignId('varietas_id')->constrained('master_varietas')->onDelete('set null');
|
||||||
|
$table->foreignId('profile_id')->constrained('profiles')->onDelete('set null');
|
||||||
|
$table->foreignId('lahan_id')->constrained('map_lahan')->onDelete('set null');
|
||||||
|
|
||||||
|
$table->float('latitude')->nullable();
|
||||||
|
$table->float('longitude')->nullable();
|
||||||
|
$table->string('kondisi_tanaman')->nullable();
|
||||||
|
$table->string('metode_pengumpulan')->nullable();
|
||||||
|
$table->text('deskripsi')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_praktik_budidaya', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignID('petani_id')->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('tenaga_kerja')->nullable();
|
||||||
|
$table->integer('frekuensi_pemangkasan_utama')->nullable();
|
||||||
|
$table->jsonb('teknik_pemangkasan')->nullable();
|
||||||
|
$table->string('frekuensi_pemangkasan_penaung')->nullable();
|
||||||
|
$table->string('penyerbukan_manual')->nullable();
|
||||||
|
$table->string('deskripsi_penyerbukan_manual')->nullable();
|
||||||
|
$table->string('tingkat_keberhasilan_penyerbukan')->nullable();
|
||||||
|
|
||||||
|
$table->integer('pemberian_pupuk')->nullable();
|
||||||
|
$table->string('jenis_pupuk')->nullable();
|
||||||
|
$table->string('frekuensi_pemberian_pupuk')->nullable();
|
||||||
|
$table->decimal('estimasi_dosis_pupuk_organik', 10, 2)->nullable();
|
||||||
|
$table->decimal('estimasi_dosis_pupuk_anorganik', 10, 2)->nullable();
|
||||||
|
$table->string('waktu_pemberian_pupuk')->nullable();
|
||||||
|
$table->string('cara_pemberian_pupuk')->nullable();
|
||||||
|
|
||||||
|
$table->integer('metode_penanganan_opt')->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->decimal('volume_obat_obatan_padat_permusim', 10, 2)->nullable();
|
||||||
|
$table->decimal('volume_obat_obatan_cair_permusim', 10, 2)->nullable();
|
||||||
|
|
||||||
|
$table->integer('frekuensi_sanitasi_lahan')->nullable();
|
||||||
|
$table->string('penanganan_sisa_buah_ranting')->nullable();
|
||||||
|
$table->integer('penanganan_gulma')->nullable();
|
||||||
|
|
||||||
|
$table->integer('kondisi_pengelolaan_air')->nullable();
|
||||||
|
$table->jsonb('sumber_air')->nullable();
|
||||||
|
$table->string('sistem_irigasi')->nullable();
|
||||||
|
$table->string('kebutuhan_air_permusim')->nullable();
|
||||||
|
|
||||||
|
$table->jsonb('kebiasaan_konservasi_tanah')->nullable();
|
||||||
|
$table->string('pemahaman_manfaat_konservasi_tanah')->nullable();
|
||||||
|
$table->integer('nilai_konservasi_tanah')->nullable();
|
||||||
|
|
||||||
|
$table->decimal('upah_tenaga_kerja', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_bibit', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_pupuk', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_obat_obatan', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_sewa_alat_produksi', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_penyediaan_air', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_produksi_lainnya', 13, 2)->nullable();
|
||||||
|
$table->decimal('total_estimasi_biaya_produksi', 13, 2)->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_survey_panen', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('set null');
|
||||||
|
|
||||||
|
$table->integer('dasar_penentuan_panen')->nullable();
|
||||||
|
$table->integer('frekuensi_panen_pertahun')->nullable();
|
||||||
|
$table->string('bulan_panen_utama')->nullable();
|
||||||
|
|
||||||
|
$table->decimal('hasil_komoditas_utama_permusim', 10, 2)->nullable();
|
||||||
|
$table->decimal('hasil_komoditas_lainnya_permusim', 10, 2)->nullable();
|
||||||
|
$table->boolean('disimpan_untuk_sendiri')->nullable();
|
||||||
|
$table->decimal('hasil_panen_disimpan', 10, 2)->nullable();
|
||||||
|
$table->string('tujuan_disimpan')->nullable();
|
||||||
|
|
||||||
|
$table->integer('praktik_sortasi')->nullable();
|
||||||
|
$table->string('bentuk_sortasi')->nullable();
|
||||||
|
|
||||||
|
$table->integer('hasil_panen_reject')->nullable();
|
||||||
|
$table->string('jumlah_panen_reject')->nullable();
|
||||||
|
$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_sewa_alat_panen', 13, 2)->nullable();
|
||||||
|
$table->decimal('biaya_panen_lainnya', 13, 2)->nullable();
|
||||||
|
$table->decimal('total_estimasi_biaya_panen', 13, 2)->nullable();
|
||||||
|
|
||||||
|
$table->string('estimasi_kehilangan_akibat_opt')->nullable();
|
||||||
|
$table->string('estimasi_kehilangan_akibat_hama_penyakit')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_survey_produktivitas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('set null');
|
||||||
|
$table->foreignId('komoditas_id')->nullable()->constrained('master_komoditas')->onDelete('set null');
|
||||||
|
|
||||||
|
$table->decimal('kuantitas')->nullable();
|
||||||
|
$table->string('satuan')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_sosial_ekonomi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->integer('jumlah_tanggungan')->nullable();
|
||||||
|
$table->integer('anak_masih_sekolah')->nullable();
|
||||||
|
$table->integer('yang_ikut_bekerja')->nullable();
|
||||||
|
|
||||||
|
$table->tinyInteger('skor_periode_paceklik')->nullable();
|
||||||
|
$table->string('bulan_paceklik')->nullable();
|
||||||
|
$table->text('sumber_penghasilan_paceklik')->nullable();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('total_estimasi_biaya_kebun_per_musim')->nullable();
|
||||||
|
$table->tinyInteger('skor_kemampuan_menabung')->nullable();
|
||||||
|
$table->unsignedBigInteger('estimasi_tabungan_per_bulan')->nullable();
|
||||||
|
|
||||||
|
$table->tinyInteger('skor_kecukupan_penghasilan')->nullable();
|
||||||
|
$table->text('alasan_kecukupan_penghasilan')->nullable();
|
||||||
|
|
||||||
|
$table->string('status_kepemilikan_lahan')->nullable();
|
||||||
|
$table->tinyInteger('skor_kejelasan_status_lahan')->nullable();
|
||||||
|
$table->boolean('ada_sertifikat_dokumen')->nullable();
|
||||||
|
$table->boolean('ada_sengketa')->nullable();
|
||||||
|
$table->text('keterangan_sengketa')->nullable();
|
||||||
|
$table->string('pihak_membantu_konflik')->nullable();
|
||||||
|
$table->string('pihak_membantu_konflik_nama')->nullable();
|
||||||
|
$table->year('tahun_bantuan_konflik')->nullable();
|
||||||
|
$table->string('status_konflik')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_pendapatan_komoditas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
|
||||||
|
$table->foreignId('sosial_ekonomi_id')->constrained('sosial_ekonomi')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('kategori')->comment('utama / lainnya');
|
||||||
|
$table->string('komoditas')->nullable();
|
||||||
|
$table->unsignedBigInteger('pendapatan')->nullable();
|
||||||
|
$table->string('periode')->nullable()->comment('musim / tahun');
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_pendapatan_luar_pertanian', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('sosial_ekonomi_id')->constrained('sosial_ekonomi')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('jenis_pekerjaan')->nullable();
|
||||||
|
$table->unsignedBigInteger('estimasi_per_bulan')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_akses_pasar', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->tinyInteger('skor_pengalaman')->nullable();
|
||||||
|
$table->json('saluran_terdahulu')->nullable();
|
||||||
|
$table->string('saluran_terdahulu_lainnya')->nullable();
|
||||||
|
$table->string('alasan_berhenti')->nullable();
|
||||||
|
|
||||||
|
$table->string('praktik_fermentasi')->nullable();
|
||||||
|
$table->text('proses_fermentasi')->nullable();
|
||||||
|
$table->string('model_fermentasi_memungkinkan')->nullable();
|
||||||
|
|
||||||
|
$table->tinyInteger('skor_kesadaran_premium')->nullable();
|
||||||
|
$table->string('sumber_info_premium')->nullable();
|
||||||
|
|
||||||
|
$table->time('waktu_mulai_wawancara')->nullable();
|
||||||
|
$table->time('waktu_selesai_wawancara')->nullable();
|
||||||
|
|
||||||
|
$table->tinyInteger('skor_kepercayaan_responden')->nullable();
|
||||||
|
$table->string('kehadiran_orang_lain')->nullable();
|
||||||
|
$table->tinyInteger('skor_pengaruh_orang_lain')->nullable();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_harga_jual_panen', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->string('bentuk_produk');
|
||||||
|
$table->integer('harga_panen_terakhir')->nullable();
|
||||||
|
$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('updated_by')->nullable()->constrained('users')->onDelete('set null');
|
||||||
|
$table->foreignId('deleted_by')->nullable()->constrained('users')->onDelete('set null');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_survey_pelatihan', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('set null');
|
||||||
|
|
||||||
|
$table->string('jenis_pelatihan');
|
||||||
|
$table->string('penyelenggara');
|
||||||
|
$table->integer('tahun_pelaksanaan');
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_survey_sertifikasi', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable();
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('set null');
|
||||||
|
|
||||||
|
$table->string('jenis_sertifikasi');
|
||||||
|
$table->string('lembaga_penerbit');
|
||||||
|
$table->integer('tahun_pelaksanaan');
|
||||||
|
$table->boolean('is_aktif')->default(false);
|
||||||
|
|
||||||
|
$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();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('riwayat_petani_skor', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('periode_pencatatan');
|
||||||
|
$table->string('uid')->nullable(); // Unique dihilangkan
|
||||||
|
|
||||||
|
$table->foreignId('petani_id')->nullable()->constrained('petani')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->float('skor_kesiapan_adopsi', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d2_sistem_praktik_budidaya', 10)->nullable();
|
||||||
|
$table->float('skor_d4_sosial_ekonomi', 10)->nullable();
|
||||||
|
$table->float('skor_d6_rantai_nilai_akses_pasar', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_komp_1_d4_k3_motivasi_kapasitas_adopsi', 10)->nullable();
|
||||||
|
$table->float('skor_komp_2_d4_k2_ekonomi_rumah_tangga', 10)->nullable();
|
||||||
|
$table->float('skor_komp_3_d2_praktik_budidaya_gap', 10)->nullable();
|
||||||
|
$table->float('skor_komp_4_d4_k4_keterlibatan_kelembagaan', 10)->nullable();
|
||||||
|
$table->float('skor_komp_5_d4_k5_k6_akses_kebebasan_keuangan', 10)->nullable();
|
||||||
|
$table->float('skor_komp_6_d6_k1_k4_k5_rantai_nilai_akses_pasar', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d4_k3_1_praktik_baru', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k3_4_pemahaman_mutu_harga', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k3_5_role_model', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k3_6_kemampuan_keterlibatan_program', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k3_7_orientasi_jangka_panjang', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d4_k2_5_stabilitas_penghasilan', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k2_8_kemampuan_menabung', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k2_9_kecukupan_penghasilan', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k2_3_k2_7_rasio_biaya_produksi_pendapatan', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k2_11_hak_kelola_lahan', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d2_k2_3_praktik_pemangkasan', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_4_praktik_pemupukan', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_5_pengendalian_opt', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_6_sanitasi_kebun', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_6b_pengelolaan_gulma', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_7_penglolaan_air_irigasi', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k2_8_praktik_konservasi_tanah', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k3_1_penentuan_pematangan', 10)->nullable();
|
||||||
|
$table->float('skor_d2_k3_5_sortasi_awal_pemisahan_kualitas', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d4_k4_2_keaktifan_pertemuan', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k4_3_peran_dalam_kelompok', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k4_4_manfaat_dirasakan', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d4_k5_1_kepemilikan_rekening', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k5_2_pengalaman_kredit_formal', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k5_3_kepemilikan_aset_produktif', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k5_4_akses_program_bantuan', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d4_k6_3_keberadaan_hutang_terkait_panen', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k6_5a_praktik_ijon', 10)->nullable();
|
||||||
|
$table->float('skor_d4_k6_5b_kebebasan_memilih_pembeli', 10)->nullable();
|
||||||
|
|
||||||
|
$table->float('skor_d6_k1_6_pengalaman_menjual_ke_saluran_berbeda', 10)->nullable();
|
||||||
|
$table->float('skor_d6_k4_1_kesadaran_akses_pasar_premium', 10)->nullable();
|
||||||
|
$table->float('skor_d6_k5_1_hambatan_mutu', 10)->nullable();
|
||||||
|
|
||||||
|
$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('riwayat_petani_skor');
|
||||||
|
Schema::dropIfExists('riwayat_survey_sertifikasi');
|
||||||
|
Schema::dropIfExists('riwayat_survey_pelatihan');
|
||||||
|
Schema::dropIfExists('riwayat_harga_jual_panen');
|
||||||
|
Schema::dropIfExists('riwayat_akses_pasar');
|
||||||
|
Schema::dropIfExists('riwayat_pendapatan_luar_pertanian');
|
||||||
|
Schema::dropIfExists('riwayat_pendapatan_komoditas');
|
||||||
|
Schema::dropIfExists('riwayat_sosial_ekonomi');
|
||||||
|
Schema::dropIfExists('riwayat_survey_produktivitas');
|
||||||
|
Schema::dropIfExists('riwayat_survey_panen');
|
||||||
|
Schema::dropIfExists('riwayat_praktik_budidaya');
|
||||||
|
Schema::dropIfExists('riwayat_map_tanaman');
|
||||||
|
Schema::dropIfExists('riwayat_sertifikat_lahan');
|
||||||
|
Schema::dropIfExists('riwayat_map_lahan');
|
||||||
|
}
|
||||||
|
};
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?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('akses_pasar', function (Blueprint $table) {
|
||||||
|
$table->text('tujuan_penjualan')->nullable();
|
||||||
|
$table->string('tujuan_penjualan_lainnya')->nullable();
|
||||||
|
$table->text('sifat_hubungan_bisnis')->nullable();
|
||||||
|
$table->text('bentuk_penjualan')->nullable();
|
||||||
|
$table->string('frekuensi_penjualan')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'tujuan_penjualan',
|
||||||
|
'tujuan_penjualan_lainnya',
|
||||||
|
'sifat_hubungan_bisnis',
|
||||||
|
'bentuk_penjualan',
|
||||||
|
'frekuensi_penjualan'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||||
|
$table->string('tahu_harga_sebelumnya')->nullable();
|
||||||
|
$table->text('sumber_info_harga')->nullable();
|
||||||
|
$table->string('sumber_info_harga_lainnya')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('akses_pasar', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'tahu_harga_sebelumnya',
|
||||||
|
'sumber_info_harga',
|
||||||
|
'sumber_info_harga_lainnya',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Penugasan user koordinator ke komoditas yang dia tangani.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('koordinator_komoditas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained('users');
|
||||||
|
$table->foreignId('komoditas_id')->constrained('master_komoditas');
|
||||||
|
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users');
|
||||||
|
$table->foreignId('updated_by')->nullable()->constrained('users');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->foreignId('deleted_by')->nullable()->constrained('users');
|
||||||
|
|
||||||
|
// Bukan unique index (sesuai AISTANDAR 5.2), cukup index untuk pencarian
|
||||||
|
$table->index(['user_id', 'komoditas_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('koordinator_komoditas');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Status kelengkapan data petani (App\Enums\StatusDataPetani):
|
||||||
|
* draft → lengkap → terverifikasi. Data lama otomatis "draft".
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('petani', function (Blueprint $table) {
|
||||||
|
$table->string('status', 20)->default('draft')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('petani', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['status']);
|
||||||
|
$table->dropColumn('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -103,3 +103,154 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar icon tiles: keep the icon mask on the inner element. */
|
||||||
|
#layout-menu .menu-inner > .menu-item > .menu-link > .sidebar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 30px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: var(--sidebar-icon-color, var(--bs-secondary));
|
||||||
|
background: var(--sidebar-icon-bg, rgba(var(--bs-secondary-rgb), 0.1));
|
||||||
|
transition: background-color 180ms ease, color 180ms ease, transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon > .ti {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-dashboard),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-database) {
|
||||||
|
--sidebar-icon-color: var(--bs-primary);
|
||||||
|
--sidebar-icon-bg: rgba(var(--bs-primary-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-user-circle),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-id-badge-2),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-plant) {
|
||||||
|
--sidebar-icon-color: var(--bs-success);
|
||||||
|
--sidebar-icon-bg: rgba(var(--bs-success-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-users-group),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-sitemap) {
|
||||||
|
--sidebar-icon-color: var(--bs-info);
|
||||||
|
--sidebar-icon-bg: rgba(var(--bs-info-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-building-community),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-heart-handshake) {
|
||||||
|
--sidebar-icon-color: var(--bs-warning);
|
||||||
|
--sidebar-icon-bg: rgba(var(--bs-warning-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-map-2),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-user-shield),
|
||||||
|
#layout-menu .sidebar-icon:has(.tabler-route) {
|
||||||
|
--sidebar-icon-color: var(--bs-primary);
|
||||||
|
--sidebar-icon-bg: rgba(var(--bs-primary-rgb), 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .menu-item > .menu-link:hover > .sidebar-icon,
|
||||||
|
#layout-menu .menu-item > .menu-link:focus-visible > .sidebar-icon {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout-menu .menu-inner > .menu-item.active > .menu-link > .sidebar-icon {
|
||||||
|
color: inherit;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#layout-menu .menu-inner > .menu-item > .menu-link > .sidebar-icon {
|
||||||
|
transition: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submenu icons replace the template's decorative bullets. */
|
||||||
|
#layout-menu .menu-sub > .menu-item > .menu-link::before { display: none; }
|
||||||
|
#layout-menu .menu-sub .sidebar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 22px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
#layout-menu .menu-sub .sidebar-icon > .ti { width: 19px; height: 19px; font-size: 19px; }
|
||||||
|
|
||||||
|
/* Profil Kelompok Tani */
|
||||||
|
.kelompok-tani-page {
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page :is(.form-control, .form-select, .form-check-label, .select2-container) {
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page select.is-invalid + .select2-container .select2-selection {
|
||||||
|
border-color: var(--bs-form-invalid-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .kelompok-tani-summary-name {
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .kelompok-tani-summary-note {
|
||||||
|
font-size: .75rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .kelompok-tani-help {
|
||||||
|
margin-top: -.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page #section-kas[hidden],
|
||||||
|
.kelompok-tani-page #section-bantuan[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page #kelompok-tani-notifications,
|
||||||
|
.kelompok-tani-page [id^="section-"] {
|
||||||
|
scroll-margin-top: 7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .select2-selection {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .select2-selection::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
inset-inline-end: 1rem;
|
||||||
|
width: .5rem;
|
||||||
|
height: .5rem;
|
||||||
|
border-right: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
transform: translateY(-75%) rotate(45deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .select2-selection__arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
padding-inline-end: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kelompok-tani-page .select2-container--disabled .select2-selection::after {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -37,10 +37,73 @@ window.config = {
|
|||||||
danger: window.Helpers.getCssVar('danger-bg-subtle'),
|
danger: window.Helpers.getCssVar('danger-bg-subtle'),
|
||||||
dark: window.Helpers.getCssVar('dark-bg-subtle')
|
dark: window.Helpers.getCssVar('dark-bg-subtle')
|
||||||
},
|
},
|
||||||
|
chartColors: {
|
||||||
|
series: ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'],
|
||||||
|
semantic: {
|
||||||
|
primary: '#9D8EF0',
|
||||||
|
secondary: '#9EA6B0',
|
||||||
|
success: '#6FD68F',
|
||||||
|
info: '#5FC7D6',
|
||||||
|
warning: '#FFB46F',
|
||||||
|
danger: '#F47C86',
|
||||||
|
purple: '#9D8EF0',
|
||||||
|
teal: '#65D8BD',
|
||||||
|
pink: '#F47C86',
|
||||||
|
gray: '#9EA6B0'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#7367F0',
|
||||||
|
secondary: '#808390',
|
||||||
|
success: '#28C76F',
|
||||||
|
info: '#00BAD1',
|
||||||
|
warning: '#FF9F43',
|
||||||
|
danger: '#FF4C51',
|
||||||
|
purple: '#7367F0',
|
||||||
|
teal: '#20C997',
|
||||||
|
pink: '#FF4C51',
|
||||||
|
gray: '#808390'
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
coffee: '#6FD68F',
|
||||||
|
vanilla: '#FFB46F',
|
||||||
|
cacao: '#9D8EF0',
|
||||||
|
candlenut: '#9EA6B0',
|
||||||
|
shade: '#65D8BD',
|
||||||
|
policyPurple: '#9D8EF0',
|
||||||
|
policyPink: '#F47C86',
|
||||||
|
marketGreen: '#6FD68F',
|
||||||
|
marketRed: '#F47C86',
|
||||||
|
marketBlue: '#5FC7D6'
|
||||||
|
},
|
||||||
|
grid: '#ECF0F6'
|
||||||
|
},
|
||||||
fontFamily: window.Helpers.getCssVar('font-family-base'),
|
fontFamily: window.Helpers.getCssVar('font-family-base'),
|
||||||
enableMenuLocalStorage: true // Enable menu state with local storage support
|
enableMenuLocalStorage: true // Enable menu state with local storage support
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Choose the label color with the higher contrast against each chart slice.
|
||||||
|
window.config.chartTextColor = function (color) {
|
||||||
|
let hex = String(color || '').trim().replace(/^#/, '');
|
||||||
|
if (/^[0-9a-f]{3}$/i.test(hex)) {
|
||||||
|
hex = hex.split('').map(value => value + value).join('');
|
||||||
|
}
|
||||||
|
if (!/^[0-9a-f]{6}$/i.test(hex)) return '#212529';
|
||||||
|
|
||||||
|
const luminance = function (value) {
|
||||||
|
const channels = value.match(/../g).map(channel => {
|
||||||
|
const srgb = parseInt(channel, 16) / 255;
|
||||||
|
return srgb <= 0.04045 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||||
|
};
|
||||||
|
const background = luminance(hex);
|
||||||
|
const dark = luminance('212529');
|
||||||
|
const darkContrast = (Math.max(background, dark) + 0.05) / (Math.min(background, dark) + 0.05);
|
||||||
|
const lightContrast = 1.05 / (background + 0.05);
|
||||||
|
|
||||||
|
return darkContrast >= lightContrast ? '#212529' : '#ffffff';
|
||||||
|
};
|
||||||
|
|
||||||
window.assetsPath = document.documentElement.getAttribute('data-assets-path');
|
window.assetsPath = document.documentElement.getAttribute('data-assets-path');
|
||||||
window.templateName = document.documentElement.getAttribute('data-template');
|
window.templateName = document.documentElement.getAttribute('data-template');
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
cardColor = config.colors.cardColor;
|
cardColor = config.colors.cardColor;
|
||||||
labelColor = config.colors.textMuted;
|
labelColor = config.colors.textMuted;
|
||||||
headingColor = config.colors.headingColor;
|
headingColor = config.colors.headingColor;
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelSemantic = pastelChart.semantic || {};
|
||||||
|
const pastelSeries = pastelChart.series || ['#9D8EF0', '#6FD68F', '#FFB46F', '#5FC7D6', '#F47C86', '#9EA6B0', '#65D8BD', '#63C7E0'];
|
||||||
|
|
||||||
// swiper loop and autoplay
|
// swiper loop and autoplay
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
@@ -48,16 +51,10 @@
|
|||||||
grid: {
|
grid: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
colors: [config.colors.success],
|
colors: [pastelSemantic.success || '#6FD68F'],
|
||||||
fill: {
|
fill: {
|
||||||
type: 'gradient',
|
type: 'solid',
|
||||||
gradient: {
|
opacity: 0.22
|
||||||
shadeIntensity: 1,
|
|
||||||
opacityFrom: 0.4,
|
|
||||||
gradientToColors: [config.colors.cardColor],
|
|
||||||
opacityTo: 0.1,
|
|
||||||
stops: [0, 100]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -151,13 +148,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: [
|
colors: [
|
||||||
config.colors_label.primary,
|
pastelSemantic.secondary || '#9EA6B0',
|
||||||
config.colors_label.primary,
|
pastelSemantic.secondary || '#9EA6B0',
|
||||||
config.colors_label.primary,
|
pastelSemantic.secondary || '#9EA6B0',
|
||||||
config.colors_label.primary,
|
pastelSemantic.secondary || '#9EA6B0',
|
||||||
config.colors.primary,
|
pastelSemantic.primary || '#9D8EF0',
|
||||||
config.colors_label.primary,
|
pastelSemantic.secondary || '#9EA6B0',
|
||||||
config.colors_label.primary
|
pastelSemantic.secondary || '#9EA6B0'
|
||||||
],
|
],
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false
|
enabled: false
|
||||||
@@ -262,18 +259,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: [config.colors.primary],
|
colors: [pastelSemantic.primary || '#9D8EF0'],
|
||||||
fill: {
|
fill: {
|
||||||
type: 'gradient',
|
type: 'solid',
|
||||||
gradient: {
|
opacity: 0.9
|
||||||
shade: 'dark',
|
|
||||||
shadeIntensity: 0.5,
|
|
||||||
gradientToColors: [config.colors.primary],
|
|
||||||
inverseColors: true,
|
|
||||||
opacityFrom: 1,
|
|
||||||
opacityTo: 0.6,
|
|
||||||
stops: [30, 70, 100]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
dashArray: 10
|
dashArray: 10
|
||||||
@@ -356,7 +345,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
colors: [config.colors.primary, config.colors.secondary],
|
colors: [
|
||||||
|
pastelSeries[0] || '#9D8EF0',
|
||||||
|
pastelSeries[1] || '#6FD68F'
|
||||||
|
],
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
@@ -369,7 +361,10 @@
|
|||||||
legend: {
|
legend: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
colors: [config.colors.primary, config.colors.secondary],
|
colors: [
|
||||||
|
pastelSeries[0] || '#9D8EF0',
|
||||||
|
pastelSeries[1] || '#6FD68F'
|
||||||
|
],
|
||||||
fill: {
|
fill: {
|
||||||
opacity: 1
|
opacity: 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
{{--
|
||||||
|
Ringkasan isian WAJIB (skor) dan PERLU (dashboard) yang belum terisi di tab petani yang sedang dibuka.
|
||||||
|
Daftar isiannya ada di App\Services\Produktivitas\KelengkapanPetani.
|
||||||
|
Klik isian → halaman menggulir ke kolomnya (dicari lewat atribut name).
|
||||||
|
--}}
|
||||||
|
@props(['petani', 'lahan' => null])
|
||||||
|
|
||||||
|
@use('App\Services\Produktivitas\KelengkapanPetani')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$tab = match (true) {
|
||||||
|
request()->routeIs('petani.edit') => 'profil',
|
||||||
|
request()->routeIs('petani.lahan-tanaman.*') => 'lahan',
|
||||||
|
request()->routeIs('petani.praktik-budidaya.*') => 'praktik-budidaya',
|
||||||
|
request()->routeIs('petani.sosial-ekonomi.*') => 'sosial-ekonomi',
|
||||||
|
request()->routeIs('petani.akses-pasar.*') => 'akses-pasar',
|
||||||
|
request()->routeIs('petani.skor.*') => 'semua',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
$urlTab = fn (string $t) => $t === 'profil'
|
||||||
|
? route('petani.edit', $petani->id)
|
||||||
|
: route('petani.' . ($t === 'lahan' ? 'lahan-tanaman' : $t) . '.index', $petani->id);
|
||||||
|
|
||||||
|
$kelengkapan = $tab ? KelengkapanPetani::untuk($petani) : null;
|
||||||
|
|
||||||
|
if ($tab === 'semua') {
|
||||||
|
// Tab Kesiapan Adopsi: rangkuman semua tab, klik → pindah ke tab terkait.
|
||||||
|
$kelompok = $kelengkapan->semuaKosong()
|
||||||
|
->map(fn ($items, $t) => $items->map(fn ($item) => [
|
||||||
|
...$item,
|
||||||
|
'url' => ($item['url'] ?? $urlTab($t)) . ($item['input'] ? '#isian=' . $item['input'] : ''),
|
||||||
|
]))
|
||||||
|
->filter(fn ($items) => $items->isNotEmpty())
|
||||||
|
->mapWithKeys(fn ($items, $t) => [KelengkapanPetani::TAB_LABEL[$t] => $items]);
|
||||||
|
} elseif ($tab) {
|
||||||
|
// Di halaman edit lahan, isian lahan yang sedang dibuka langsung digulir (tanpa pindah halaman).
|
||||||
|
$lahanDibukaId = $tab === 'lahan' && request()->routeIs('petani.lahan-tanaman.edit') ? $lahan?->id : null;
|
||||||
|
|
||||||
|
$kelompok = $kelengkapan->kosong($tab)
|
||||||
|
->map(function ($item) use ($lahanDibukaId) {
|
||||||
|
$dibuka = $lahanDibukaId && $item['lahan_id'] === $lahanDibukaId;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...$item,
|
||||||
|
'grup' => $dibuka ? $item['grup'] . ' (sedang dibuka)' : $item['grup'],
|
||||||
|
'url' => ($dibuka || ! $item['url']) ? null : $item['url'] . ($item['input'] ? '#isian=' . $item['input'] : ''),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->groupBy(fn ($item) => $item['grup'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$semuaItem = $tab ? $kelompok->flatten(1) : collect();
|
||||||
|
$jumlahWajib = $semuaItem->where('prioritas', KelengkapanPetani::WAJIB)->count();
|
||||||
|
$jumlahPerlu = $semuaItem->where('prioritas', KelengkapanPetani::PERLU)->count();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
{{-- Kartu hanya muncul kalau masih ada yang kosong; tab yang sudah lengkap tidak diberi kotak tambahan. --}}
|
||||||
|
@if ($tab && $semuaItem->isNotEmpty())
|
||||||
|
{{-- Klik di mana saja pada kartu untuk membuka/menutup detail (kecuali tombol isian). --}}
|
||||||
|
<div class="card mb-6 border border-warning kartu-kelengkapan" id="kelengkapan-petani"
|
||||||
|
role="button" aria-expanded="false" aria-controls="kelengkapan-petani-detail" title="Klik untuk menampilkan/menyembunyikan detail">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="icon-base ti tabler-alert-triangle text-warning me-2"></i>
|
||||||
|
<span class="fw-medium">
|
||||||
|
Belum terisi{{ $tab === 'semua' ? '' : ' di tab ini' }}:
|
||||||
|
@if ($jumlahWajib)
|
||||||
|
<span class="badge bg-label-danger ms-1">{{ $jumlahWajib }} wajib</span>
|
||||||
|
@endif
|
||||||
|
@if ($jumlahPerlu)
|
||||||
|
<span class="badge bg-label-warning ms-1">{{ $jumlahPerlu }} perlu</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="btn btn-sm btn-icon btn-text-secondary rounded-pill ikon-toggle-kelengkapan" aria-hidden="true">
|
||||||
|
<i class="icon-base ti tabler-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse" id="kelengkapan-petani-detail">
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<span class="badge badge-dot bg-danger me-1"></span>Wajib: dipakai skor Kesiapan Adopsi
|
||||||
|
<span class="badge badge-dot bg-warning ms-3 me-1"></span>Perlu: dipakai dashboard.
|
||||||
|
Klik isian untuk menuju kolomnya.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
@foreach ($kelompok as $judul => $items)
|
||||||
|
<div class="mt-3">
|
||||||
|
@if ($judul !== '')
|
||||||
|
<div class="small fw-medium mb-1">{{ $judul }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach ($items->sortBy(fn ($i) => $i['prioritas'] === KelengkapanPetani::WAJIB ? 0 : 1) as $item)
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-xs btn-isian-kosong {{ $item['prioritas'] === KelengkapanPetani::WAJIB ? 'btn-label-danger' : 'btn-label-warning' }}"
|
||||||
|
data-input="{{ $item['input'] }}"
|
||||||
|
data-url="{{ $item['url'] }}">
|
||||||
|
{{ $item['label'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@once
|
||||||
|
@push('style')
|
||||||
|
<style>
|
||||||
|
.kartu-kelengkapan { cursor: pointer; }
|
||||||
|
.kartu-kelengkapan .btn-isian-kosong { cursor: pointer; }
|
||||||
|
.ikon-toggle-kelengkapan i { transition: transform .2s; }
|
||||||
|
.kartu-kelengkapan.terbuka .ikon-toggle-kelengkapan i { transform: rotate(180deg); }
|
||||||
|
.isian-disorot {
|
||||||
|
outline: 2px solid var(--bs-warning);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: .375rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function cariKolom(name) {
|
||||||
|
if (!name) return null;
|
||||||
|
const el = document.querySelector(`[name="${name}"], [name="${name}[]"], [name^="${name}["]`);
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
// Kolom tersembunyi (mis. radio custom/select2): sorot pembungkus terdekat yang terlihat.
|
||||||
|
let target = el.closest('.col-md-12, .col-md-6, .col-md-4, .col-12, .col, .mb-4, .mb-6') || el;
|
||||||
|
while (target && target.offsetParent === null && target.parentElement) {
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
return { el, target };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sorot(name) {
|
||||||
|
const hasil = cariKolom(name);
|
||||||
|
if (!hasil) return false;
|
||||||
|
|
||||||
|
hasil.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
hasil.target.classList.add('isian-disorot');
|
||||||
|
setTimeout(() => hasil.target.classList.remove('isian-disorot'), 2500);
|
||||||
|
if (hasil.el.offsetParent !== null && typeof hasil.el.focus === 'function') {
|
||||||
|
hasil.el.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buka/tutup detail dengan klik di mana saja pada kartu.
|
||||||
|
const kartu = document.getElementById('kelengkapan-petani');
|
||||||
|
const detail = document.getElementById('kelengkapan-petani-detail');
|
||||||
|
if (kartu && detail) {
|
||||||
|
kartu.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('.btn-isian-kosong')) return;
|
||||||
|
if (window.getSelection().toString()) return; // sedang menyeleksi teks
|
||||||
|
bootstrap.Collapse.getOrCreateInstance(detail, { toggle: false }).toggle();
|
||||||
|
});
|
||||||
|
detail.addEventListener('show.bs.collapse', () => { kartu.classList.add('terbuka'); kartu.setAttribute('aria-expanded', 'true'); });
|
||||||
|
detail.addEventListener('hide.bs.collapse', () => { kartu.classList.remove('terbuka'); kartu.setAttribute('aria-expanded', 'false'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const btn = e.target.closest('.btn-isian-kosong');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
if (btn.dataset.url) {
|
||||||
|
window.location.href = btn.dataset.url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sorot(btn.dataset.input)) {
|
||||||
|
const pesan = 'Kolom ini belum tampil di halaman ini. Simpan bagian yang sudah ada terlebih dahulu, atau periksa jawaban pertanyaan sebelumnya.';
|
||||||
|
window.Swal ? Swal.fire({ icon: 'info', text: pesan, buttonsStyling: false, customClass: { confirmButton: 'btn btn-primary' } }) : alert(pesan);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Datang dari tab lain dengan #isian=nama_kolom
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
const cocok = window.location.hash.match(/^#isian=(.+)$/);
|
||||||
|
if (cocok) setTimeout(() => sorot(decodeURIComponent(cocok[1])), 300);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endonce
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{{--
|
||||||
|
Kelengkapan data (terisi X dari total isian) + status Draf/Lengkap/Terverifikasi di kartu Profil Petani.
|
||||||
|
Tombol "Tandai Lengkap" hanya untuk enumerator penginput, dan nonaktif selama isian wajib masih kosong.
|
||||||
|
--}}
|
||||||
|
@props(['petani'])
|
||||||
|
|
||||||
|
@use('App\Enums\StatusDataPetani')
|
||||||
|
@use('App\Services\Produktivitas\KelengkapanPetani')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$kelengkapan = KelengkapanPetani::untuk($petani);
|
||||||
|
$hitung = $kelengkapan->hitungIsian();
|
||||||
|
$wajibKosong = $kelengkapan->jumlahWajibKosong();
|
||||||
|
$status = $petani->status ?? StatusDataPetani::DRAFT;
|
||||||
|
$bisaTandai = $petani->bisaDitandaiOleh(auth()->user());
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@php
|
||||||
|
// Rincian dipindah ke tooltip supaya kartu Profil tetap pendek (sejajar kartu ringkasan di kanan).
|
||||||
|
$rincian = collect([
|
||||||
|
"{$hitung['terisi']} dari {$hitung['total']} isian terisi",
|
||||||
|
$hitung['tidak_berlaku'] ? "{$hitung['tidak_berlaku']} isian tidak berlaku untuk petani ini" : null,
|
||||||
|
$wajibKosong ? "{$wajibKosong} isian wajib masih kosong" : 'Semua isian wajib sudah terisi',
|
||||||
|
match (true) {
|
||||||
|
$bisaTandai => null,
|
||||||
|
$status === StatusDataPetani::DRAFT => 'Hanya enumerator penginput yang dapat menandai lengkap',
|
||||||
|
$status === StatusDataPetani::LENGKAP => 'Menunggu verifikasi koordinator',
|
||||||
|
default => null,
|
||||||
|
},
|
||||||
|
$bisaTandai && $status === StatusDataPetani::LENGKAP ? 'Mengubah data petani akan mengembalikan status ke Draf' : null,
|
||||||
|
])->filter()->implode(' · ');
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<hr class="my-2">
|
||||||
|
<div title="{{ $rincian }}" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||||
|
<div class="d-flex justify-content-between align-items-center" style="font-size: 0.75rem;">
|
||||||
|
<span class="d-flex align-items-center gap-1">
|
||||||
|
<span class="fw-medium text-heading">Kelengkapan</span>
|
||||||
|
<span class="badge badge-sm bg-label-{{ $status->warna() }}">{{ $status->label() }}</span>
|
||||||
|
@if ($wajibKosong)
|
||||||
|
<span class="badge badge-sm bg-label-danger">{{ $wajibKosong }} wajib</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
<span><span class="text-muted">{{ $hitung['terisi'] }}/{{ $hitung['total'] }}</span> <span class="fw-medium">{{ $hitung['persen'] }}%</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-1" style="height: 4px;">
|
||||||
|
<div class="progress-bar {{ $hitung['persen'] >= 100 ? 'bg-success' : '' }}" role="progressbar"
|
||||||
|
style="width: {{ $hitung['persen'] }}%;" aria-valuenow="{{ $hitung['persen'] }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($bisaTandai && $status === StatusDataPetani::DRAFT)
|
||||||
|
<form method="POST" action="{{ route('petani.status.update', $petani->id) }}" class="form-status-petani mt-2"
|
||||||
|
data-judul="Pernyataan Kelengkapan Data"
|
||||||
|
data-konfirmasi="Setelah ditandai lengkap, data akan diteruskan ke koordinator untuk diverifikasi."
|
||||||
|
data-pernyataan="Saya menyatakan bahwa data petani {{ $petani->nama }} telah saya periksa, sudah lengkap, dan sesuai dengan hasil survei bersama pihak terkait di lapangan. Saya bertanggung jawab atas kebenaran data ini."
|
||||||
|
data-tombol="Ya, Tandai Lengkap">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="status" value="{{ StatusDataPetani::LENGKAP->value }}">
|
||||||
|
<input type="hidden" name="pernyataan" value="0">
|
||||||
|
<button type="submit" class="btn btn-xs btn-primary w-100" @disabled($wajibKosong > 0)
|
||||||
|
@if ($wajibKosong) title="Lengkapi {{ $wajibKosong }} isian wajib terlebih dahulu" @endif>
|
||||||
|
<i class="icon-base ti tabler-checks icon-sm me-1"></i>Tandai Sudah Lengkap
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@elseif ($bisaTandai && $status === StatusDataPetani::LENGKAP)
|
||||||
|
<form method="POST" action="{{ route('petani.status.update', $petani->id) }}" class="form-status-petani mt-2"
|
||||||
|
data-konfirmasi="Batalkan tanda lengkap? Status kembali ke draf.">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
<input type="hidden" name="status" value="{{ StatusDataPetani::DRAFT->value }}">
|
||||||
|
<button type="submit" class="btn btn-xs btn-outline-secondary w-100">
|
||||||
|
<i class="icon-base ti tabler-arrow-back-up icon-sm me-1"></i>Batalkan Tanda Lengkap
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@once
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
// Tombol popup mengikuti warna tombol aplikasi (bukan ungu bawaan SweetAlert).
|
||||||
|
const gayaTombolSwal = () => ({
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: { confirmButton: 'btn btn-primary me-2', cancelButton: 'btn btn-label-secondary' },
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
const form = e.target.closest('.form-status-petani');
|
||||||
|
if (!form || form.dataset.terkonfirmasi) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const pernyataan = form.dataset.pernyataan;
|
||||||
|
const kirim = () => {
|
||||||
|
if (pernyataan) form.querySelector('input[name="pernyataan"]').value = '1';
|
||||||
|
form.dataset.terkonfirmasi = '1';
|
||||||
|
form.submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!window.Swal) {
|
||||||
|
if (confirm((pernyataan ? pernyataan + '\n\n' : '') + form.dataset.konfirmasi)) kirim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opsi = {
|
||||||
|
icon: pernyataan ? 'warning' : 'question',
|
||||||
|
title: form.dataset.judul || undefined,
|
||||||
|
text: form.dataset.konfirmasi,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: form.dataset.tombol || 'Ya',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
...gayaTombolSwal(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tandai Lengkap: wajib mencentang pernyataan sebelum bisa lanjut.
|
||||||
|
if (pernyataan) {
|
||||||
|
opsi.input = 'checkbox';
|
||||||
|
opsi.inputValue = 0;
|
||||||
|
opsi.inputPlaceholder = pernyataan;
|
||||||
|
opsi.inputValidator = (dicentang) => !dicentang && 'Centang pernyataan terlebih dahulu.';
|
||||||
|
opsi.customClass.input = 'text-start small';
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire(opsi).then((r) => { if (r.isConfirmed) kirim(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
@if (session('status_petani_info'))
|
||||||
|
// Data diubah setelah ditandai lengkap → status kembali ke Draf (KembalikanStatusPetaniKeDraf).
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
const pesan = @json(session('status_petani_info'));
|
||||||
|
window.Swal ? Swal.fire({ icon: 'info', title: 'Status kembali ke Draf', text: pesan, ...gayaTombolSwal() }) : alert(pesan);
|
||||||
|
});
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (session('status_petani_success') || session('status_petani_error') || $errors->has('pernyataan'))
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
const pesan = @json(session('status_petani_success') ?? session('status_petani_error') ?? $errors->first('pernyataan'));
|
||||||
|
const ikon = @json(session('status_petani_success') ? 'success' : 'error');
|
||||||
|
window.Swal ? Swal.fire({ icon: ikon, text: pesan, ...gayaTombolSwal() }) : alert(pesan);
|
||||||
|
});
|
||||||
|
@endif
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
@endonce
|
||||||
@@ -238,6 +238,10 @@
|
|||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelSemantic = pastelChart.semantic || {};
|
||||||
|
const pastelCategory = pastelChart.categories || {};
|
||||||
|
const gridColor = pastelChart.grid || '#E8EDF5';
|
||||||
|
|
||||||
// 1. Inisialisasi Icons
|
// 1. Inisialisasi Icons
|
||||||
if(typeof lucide !== 'undefined') {
|
if(typeof lucide !== 'undefined') {
|
||||||
@@ -255,7 +259,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Desa Mengadopsi (Realisasi)',
|
label: 'Desa Mengadopsi (Realisasi)',
|
||||||
data: [0, 3, 8, 15], // Kumulatif: 3 Desa Inti + 12 Baru
|
data: [0, 3, 8, 15], // Kumulatif: 3 Desa Inti + 12 Baru
|
||||||
backgroundColor: '#8b5cf6', // Purple-500
|
backgroundColor: pastelCategory.policyPurple || '#9D8EF0',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
order: 2
|
order: 2
|
||||||
},
|
},
|
||||||
@@ -263,12 +267,12 @@
|
|||||||
label: 'Target Perluasan ToC',
|
label: 'Target Perluasan ToC',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: [3, 5, 10, 15],
|
data: [3, 5, 10, 15],
|
||||||
borderColor: '#d946ef', // Fuchsia-500
|
borderColor: pastelCategory.policyPink || '#F47C86',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
pointBackgroundColor: '#d946ef',
|
pointBackgroundColor: pastelCategory.policyPink || '#F47C86',
|
||||||
order: 1
|
order: 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -280,7 +284,7 @@
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: { color: '#f3f4f6' },
|
grid: { color: gridColor },
|
||||||
ticks: { stepSize: 5 }
|
ticks: { stepSize: 5 }
|
||||||
},
|
},
|
||||||
x: { grid: { display: false } }
|
x: { grid: { display: false } }
|
||||||
@@ -309,10 +313,10 @@
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
data: [4, 2, 1, 1],
|
data: [4, 2, 1, 1],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'#f59e0b', // Amber-500
|
pastelSemantic.warning || '#FFB46F',
|
||||||
'#10b981', // Emerald-500
|
pastelSemantic.success || '#6FD68F',
|
||||||
'#3b82f6', // Blue-500
|
pastelSemantic.info || '#5FC7D6',
|
||||||
'#8b5cf6' // Purple-500
|
pastelCategory.policyPurple || '#9D8EF0'
|
||||||
],
|
],
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ffffff',
|
borderColor: '#ffffff',
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
transition: transform .2s ease;
|
transition: transform .2s ease;
|
||||||
}
|
}
|
||||||
.tk-stat:hover { transform: translateX(4px); }
|
.tk-stat:hover { transform: translateX(4px); }
|
||||||
.tk-stat.green { border-left-color: #28a745; }
|
.tk-stat.green { border-left-color: #6FD68F; }
|
||||||
.tk-stat.blue { border-left-color: #007bff; }
|
.tk-stat.blue { border-left-color: #5FC7D6; }
|
||||||
.tk-stat.amber { border-left-color: #fd7e14; }
|
.tk-stat.amber { border-left-color: #FFB46F; }
|
||||||
|
|
||||||
.tk-stat-lbl {
|
.tk-stat-lbl {
|
||||||
font-size: 10px; text-transform: uppercase;
|
font-size: 10px; text-transform: uppercase;
|
||||||
@@ -88,38 +88,38 @@
|
|||||||
'new_month' => 12,
|
'new_month' => 12,
|
||||||
|
|
||||||
'enumerators' => [
|
'enumerators' => [
|
||||||
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#28a745','bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
|
['name'=>'Enumerator Ngada 1','done'=>72,'target'=>80,'color'=>'#6FD68F','bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
|
||||||
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#007bff','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
|
['name'=>'Enumerator Ngada 2','done'=>58,'target'=>80,'color'=>'#5FC7D6','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
|
||||||
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#fd7e14','bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
|
['name'=>'Enumerator Ngada 3','done'=>48,'target'=>80,'color'=>'#FFB46F','bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
|
||||||
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#dc3545','bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
|
['name'=>'Enumerator Ngada 4','done'=>36,'target'=>80,'color'=>'#F47C86','bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'recent' => [
|
'recent' => [
|
||||||
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
|
['code'=>'TKD-2026-0178','name'=>'Markus Bela', 'init'=>'MB','desa'=>'Wangka', 'date'=>'24 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
|
||||||
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
|
['code'=>'TKD-2026-0177','name'=>'Theresia Nuka','init'=>'TN','desa'=>'Langa', 'date'=>'23 Jun 2026','pct'=>67, 'status'=>'in_progress','bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
|
||||||
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(40,167,69,.15)','tx'=>'#28a745'],
|
['code'=>'TKD-2026-0176','name'=>'Yosef Meo', 'init'=>'YM','desa'=>'Bobo', 'date'=>'22 Jun 2026','pct'=>100,'status'=>'completed', 'bg'=>'rgba(111,214,143,.24)','tx'=>'#6FD68F'],
|
||||||
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
|
['code'=>'TKD-2026-0175','name'=>'Maria Seda', 'init'=>'MS','desa'=>'Tana Wolo','date'=>'21 Jun 2026','pct'=>22, 'status'=>'draft', 'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
|
||||||
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(220,53,69,.15)','tx'=>'#dc3545'],
|
['code'=>'TKD-2026-0174','name'=>'Rafael Bolo', 'init'=>'RB','desa'=>'Wangka', 'date'=>'20 Jun 2026','pct'=>11, 'status'=>'draft', 'bg'=>'rgba(244,124,134,.26)','tx'=>'#F47C86'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'komoditas' => [
|
'komoditas' => [
|
||||||
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#28a745','arc'=>174],
|
['label'=>'Kopi', 'total'=>148,'pct'=>60,'color'=>'#6FD68F','arc'=>174],
|
||||||
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#007bff','arc'=>96],
|
['label'=>'Kakao', 'total'=>82, 'pct'=>33,'color'=>'#9D8EF0','arc'=>96],
|
||||||
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#fd7e14','arc'=>38],
|
['label'=>'Vanili','total'=>33, 'pct'=>13,'color'=>'#FFB46F','arc'=>38],
|
||||||
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#6f42c1','arc'=>26],
|
['label'=>'Kemiri','total'=>22, 'pct'=>9, 'color'=>'#9EA6B0','arc'=>26],
|
||||||
],
|
],
|
||||||
|
|
||||||
'kecamatan' => [
|
'kecamatan' => [
|
||||||
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#28a745'],
|
['name'=>'Bajawa','total'=>89,'pct'=>78,'color'=>'#6FD68F'],
|
||||||
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#007bff'],
|
['name'=>'Soa', 'total'=>68,'pct'=>59,'color'=>'#5FC7D6'],
|
||||||
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#fd7e14'],
|
['name'=>'Golewa','total'=>57,'pct'=>50,'color'=>'#FFB46F'],
|
||||||
],
|
],
|
||||||
|
|
||||||
'pending' => [
|
'pending' => [
|
||||||
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(0,123,255,.15)','tx'=>'#007bff'],
|
['name'=>'Theresia Nuka','section'=>'D4-K3','pct'=>67,'bg'=>'rgba(95,199,214,.24)','tx'=>'#5FC7D6'],
|
||||||
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(253,126,20,.15)','tx'=>'#fd7e14'],
|
['name'=>'Maria Seda', 'section'=>'D2-K2','pct'=>22,'bg'=>'rgba(255,180,111,.26)','tx'=>'#FFB46F'],
|
||||||
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
|
['name'=>'Rafael Bolo', 'section'=>'D2-K1','pct'=>11,'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
|
||||||
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(220,53,69,.15)', 'tx'=>'#dc3545'],
|
['name'=>'Antonius Wea', 'section'=>'D2-K1','pct'=>0, 'bg'=>'rgba(244,124,134,.26)', 'tx'=>'#F47C86'],
|
||||||
],
|
],
|
||||||
'pending_more' => 32,
|
'pending_more' => 32,
|
||||||
];
|
];
|
||||||
@@ -258,10 +258,10 @@
|
|||||||
@foreach($d['recent'] as $s)
|
@foreach($d['recent'] as $s)
|
||||||
@php
|
@php
|
||||||
$bc = match(true) {
|
$bc = match(true) {
|
||||||
$s['pct'] === 100 => '#28a745',
|
$s['pct'] === 100 => '#6FD68F',
|
||||||
$s['pct'] >= 50 => '#007bff',
|
$s['pct'] >= 50 => '#5FC7D6',
|
||||||
$s['pct'] >= 20 => '#fd7e14',
|
$s['pct'] >= 20 => '#FFB46F',
|
||||||
default => '#dc3545',
|
default => '#F47C86',
|
||||||
};
|
};
|
||||||
@endphp
|
@endphp
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -208,6 +208,10 @@
|
|||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelSemantic = pastelChart.semantic || {};
|
||||||
|
const pastelCategory = pastelChart.categories || {};
|
||||||
|
const gridColor = pastelChart.grid || '#E8EDF5';
|
||||||
|
|
||||||
// 1. Inisialisasi Icons
|
// 1. Inisialisasi Icons
|
||||||
if(typeof lucide !== 'undefined') {
|
if(typeof lucide !== 'undefined') {
|
||||||
@@ -225,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Mentah/Gelondong (Rp)',
|
label: 'Mentah/Gelondong (Rp)',
|
||||||
data: [35000, 200000, 25000, 15000],
|
data: [35000, 200000, 25000, 15000],
|
||||||
backgroundColor: '#9ca3af', // Gray
|
backgroundColor: pastelSemantic.gray || '#9EA6B0',
|
||||||
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
|
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
|
||||||
barPercentage: 0.6,
|
barPercentage: 0.6,
|
||||||
categoryPercentage: 0.8
|
categoryPercentage: 0.8
|
||||||
@@ -233,7 +237,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Produk Olahan (Rp)',
|
label: 'Produk Olahan (Rp)',
|
||||||
data: [120000, 1500000, 80000, 45000],
|
data: [120000, 1500000, 80000, 45000],
|
||||||
backgroundColor: '#d97706', // Amber-600
|
backgroundColor: pastelSemantic.warning || '#FFB46F',
|
||||||
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
|
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 },
|
||||||
barPercentage: 0.6,
|
barPercentage: 0.6,
|
||||||
categoryPercentage: 0.8
|
categoryPercentage: 0.8
|
||||||
@@ -246,7 +250,7 @@
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: { color: '#f3f4f6' },
|
grid: { color: gridColor },
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function(value) {
|
callback: function(value) {
|
||||||
if (value >= 1000000) return 'Rp ' + (value/1000000) + ' Jt';
|
if (value >= 1000000) return 'Rp ' + (value/1000000) + ' Jt';
|
||||||
@@ -282,8 +286,8 @@
|
|||||||
{
|
{
|
||||||
label: 'Kopi Bubuk/Roasted',
|
label: 'Kopi Bubuk/Roasted',
|
||||||
data: [12, 15, 18, 25, 30, 45],
|
data: [12, 15, 18, 25, 30, 45],
|
||||||
borderColor: '#6F4E37',
|
borderColor: pastelCategory.coffee || '#6FD68F',
|
||||||
backgroundColor: 'rgba(111, 78, 55, 0.1)',
|
backgroundColor: 'rgba(104, 193, 124, 0.16)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
@@ -291,7 +295,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Kakao Fermentasi',
|
label: 'Kakao Fermentasi',
|
||||||
data: [5, 8, 12, 15, 22, 28],
|
data: [5, 8, 12, 15, 22, 28],
|
||||||
borderColor: '#D2691E',
|
borderColor: pastelCategory.cacao || '#9D8EF0',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
@@ -305,7 +309,7 @@
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
title: { display: true, text: 'Volume (Ton)' },
|
title: { display: true, text: 'Volume (Ton)' },
|
||||||
grid: { color: '#f3f4f6' }
|
grid: { color: gridColor }
|
||||||
},
|
},
|
||||||
x: { grid: { display: false } }
|
x: { grid: { display: false } }
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -212,6 +212,9 @@
|
|||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelCategory = pastelChart.categories || {};
|
||||||
|
const gridColor = pastelChart.grid || '#E8EDF5';
|
||||||
|
|
||||||
// 1. Inisialisasi Icons
|
// 1. Inisialisasi Icons
|
||||||
if(typeof lucide !== 'undefined') {
|
if(typeof lucide !== 'undefined') {
|
||||||
@@ -230,8 +233,8 @@
|
|||||||
label: 'Total Produksi Lahan (Ton)',
|
label: 'Total Produksi Lahan (Ton)',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: [100, 110, 125, 145],
|
data: [100, 110, 125, 145],
|
||||||
borderColor: '#10b981', // Emerald-500
|
borderColor: pastelCategory.marketGreen || '#6FD68F',
|
||||||
backgroundColor: '#10b981',
|
backgroundColor: pastelCategory.marketGreen || '#6FD68F',
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
fill: false
|
fill: false
|
||||||
@@ -239,14 +242,14 @@
|
|||||||
{
|
{
|
||||||
label: 'Tengkulak / Pasar Bebas',
|
label: 'Tengkulak / Pasar Bebas',
|
||||||
data: [80, 65, 40, 25],
|
data: [80, 65, 40, 25],
|
||||||
backgroundColor: '#ef4444', // Red-500
|
backgroundColor: pastelCategory.marketRed || '#F47C86',
|
||||||
stack: 'Stack 0',
|
stack: 'Stack 0',
|
||||||
borderRadius: { topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 4 }
|
borderRadius: { topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 4 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kelembagaan (Koperasi/BUMDes)',
|
label: 'Kelembagaan (Koperasi/BUMDes)',
|
||||||
data: [20, 45, 85, 120],
|
data: [20, 45, 85, 120],
|
||||||
backgroundColor: '#3b82f6', // Blue-500
|
backgroundColor: pastelCategory.marketBlue || '#5FC7D6',
|
||||||
stack: 'Stack 0',
|
stack: 'Stack 0',
|
||||||
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 }
|
borderRadius: { topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0 }
|
||||||
}
|
}
|
||||||
@@ -260,7 +263,7 @@
|
|||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: { color: '#f3f4f6' }
|
grid: { color: gridColor }
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
@@ -292,20 +295,20 @@
|
|||||||
{
|
{
|
||||||
label: 'Kopi & Kakao',
|
label: 'Kopi & Kakao',
|
||||||
data: [150, 280, 320, 500, 650, 850],
|
data: [150, 280, 320, 500, 650, 850],
|
||||||
borderColor: '#6F4E37',
|
borderColor: pastelCategory.coffee || '#6FD68F',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointBackgroundColor: '#6F4E37'
|
pointBackgroundColor: pastelCategory.coffee || '#6FD68F'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Vanili & Kemiri',
|
label: 'Vanili & Kemiri',
|
||||||
data: [50, 120, 210, 350, 420, 600],
|
data: [50, 120, 210, 350, 420, 600],
|
||||||
borderColor: '#eab308',
|
borderColor: pastelCategory.vanilla || '#FFB46F',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointBackgroundColor: '#eab308'
|
pointBackgroundColor: pastelCategory.vanilla || '#FFB46F'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -316,7 +319,7 @@
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
title: { display: false },
|
title: { display: false },
|
||||||
grid: { color: '#f3f4f6' },
|
grid: { color: gridColor },
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function(value) {
|
callback: function(value) {
|
||||||
return 'Rp ' + value + ' Jt';
|
return 'Rp ' + value + ' Jt';
|
||||||
|
|||||||
@@ -0,0 +1,965 @@
|
|||||||
|
@extends('layout.main')
|
||||||
|
|
||||||
|
@push('style')
|
||||||
|
<style>
|
||||||
|
.pasar-filter-card .filter-item {
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-kpi-value {
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-kpi-label {
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-kpi-desc {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-header-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.08);
|
||||||
|
border-inline-start: 4px solid var(--bs-primary, #9D8EF0);
|
||||||
|
box-shadow: none;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-header-toggle:hover {
|
||||||
|
background-color: rgba(var(--bs-primary-rgb, 115, 103, 240), 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-header-toggle .card-body {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-header-toggle h5 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-header-toggle:focus-visible {
|
||||||
|
outline: 2px solid var(--bs-primary);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-icon-toggle {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-icon-toggle .icon-base {
|
||||||
|
font-size: 24px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-group-panel.show,
|
||||||
|
.chart-group-panel.collapsing {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart-card .card-title h5 {
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart-card .card-title small,
|
||||||
|
.chart-group-header-toggle p {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart {
|
||||||
|
min-height: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart-empty {
|
||||||
|
min-height: 330px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart-card .apexcharts-tooltip {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
white-space: normal;
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-chart-tooltip {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, .15);
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pasar-trend-tooltip-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.pasar-filter-card .filter-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-info-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 20px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--bs-secondary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.market-info-button:hover, .market-info-button:focus-visible {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
outline: 2px solid rgba(var(--bs-primary-rgb), .2);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.international-provider-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.international-provider-logo[hidden] { display: none; }
|
||||||
|
.international-provider-logo img { object-fit: contain; }
|
||||||
|
.international-market-track {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding: 2px 2px 16px;
|
||||||
|
}
|
||||||
|
.international-market-track::-webkit-scrollbar { display: none; }
|
||||||
|
.international-market-card {
|
||||||
|
flex: 0 0 calc((100% - 2.5rem) / 3);
|
||||||
|
min-width: 0;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.international-market-card { flex-basis: calc((100% - 1.25rem) / 2); }
|
||||||
|
}
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.international-market-card { flex-basis: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-xxl flex-grow-1 container-p-y">
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<form id="form-filter" class="col-12" method="GET" action="{{ route('dashboard.pasar') }}">
|
||||||
|
<div class="card card-border-shadow-primary pasar-filter-card">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<div class="d-flex align-items-center me-2">
|
||||||
|
<i class="icon-base ti tabler-map-2 me-2"></i>
|
||||||
|
<span class="fw-medium">Wilayah</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-provinsi" class="form-select form-select-sm" name="provinsi_id" data-default="{{ $provinsi_id }}">
|
||||||
|
<option value="">Semua Provinsi</option>
|
||||||
|
@foreach($provinsi as $p)
|
||||||
|
<option value="{{ $p->provinsi_id }}" {{ old('provinsi_id', $provinsi_id) == $p->provinsi_id ? 'selected' : '' }}>
|
||||||
|
{{ $p->provinsi }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-kabkota" class="form-select form-select-sm" name="kabupatenKota_id" data-default="{{ $kabupatenKota_id }}" disabled>
|
||||||
|
<option value="">Semua Kabupaten/Kota</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-kecamatan" class="form-select form-select-sm" name="kecamatan_id" data-default="{{ $kecamatan_id }}" disabled>
|
||||||
|
<option value="">Semua Kecamatan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-desa" class="form-select form-select-sm" name="desaKelurahan_id" data-default="{{ $desaKelurahan_id }}" disabled>
|
||||||
|
<option value="">Semua Desa/Kelurahan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-3">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<div class="d-flex align-items-center me-2">
|
||||||
|
<i class="icon-base ti tabler-adjustments-horizontal me-2"></i>
|
||||||
|
<span class="fw-medium">Data</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-komoditas" class="form-select form-select-sm" name="komoditas_id">
|
||||||
|
<option value="">Semua Komoditas</option>
|
||||||
|
@foreach ($komoditas as $item)
|
||||||
|
<option value="{{ $item->id }}" {{ old('komoditas_id', $komoditas_id) == $item->id ? 'selected' : '' }}>
|
||||||
|
{{ $item->nama }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<select id="filter-segmen" class="form-select form-select-sm" name="segmen">
|
||||||
|
<option value="">Semua Segmen</option>
|
||||||
|
@foreach ($segments as $segment)
|
||||||
|
<option value="{{ $segment['name'] }}" {{ old('segmen', $segmen) == $segment['name'] ? 'selected' : '' }}>
|
||||||
|
{{ $segment['name'] }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="icon-base ti tabler-filter me-1"></i>
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
<button type="button" id="btn-reset-dashboard-filter" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="icon-base ti tabler-refresh me-1"></i>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
@foreach ($priceCards as $priceCard)
|
||||||
|
<div class="col-12 col-md-6 col-xl-3">
|
||||||
|
<div class="card card-border-shadow-{{ $priceCard['variant'] }} h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-column align-items-start gap-3">
|
||||||
|
<div class="avatar avatar-lg">
|
||||||
|
<span class="avatar-initial rounded bg-label-{{ $priceCard['variant'] }}">
|
||||||
|
<i class="icon-base ti {{ $priceCard['icon'] }} icon-28px"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-baseline gap-2 flex-wrap">
|
||||||
|
<h4 class="mb-0 pasar-kpi-value">{{ $priceCard['value'] }}</h4>
|
||||||
|
<small class="text-body-secondary">{{ $priceCard['unit'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-0 mt-1 pasar-kpi-label">{{ $priceCard['label'] }}</p>
|
||||||
|
<small class="text-body-secondary pasar-kpi-desc">{{ $priceCard['description'] }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-3 border-top d-flex align-items-center justify-content-between gap-2 flex-wrap">
|
||||||
|
<span class="badge bg-label-{{ $priceCard['variant'] }}">{{ $priceCard['records'] > 0 ? 'Rata-rata harga' : 'Belum ada data' }}</span>
|
||||||
|
<small class="text-body-secondary">{{ number_format($priceCard['records'], 0, ',', '.') }} data harga</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
|
||||||
|
data-bs-toggle="collapse" data-bs-target="#saluran-harga-charts"
|
||||||
|
aria-expanded="false" aria-controls="saluran-harga-charts" aria-label="Tampilkan chart">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<h5>Saluran & Harga</h5>
|
||||||
|
<p class="mb-0 text-body-secondary">Saluran penjualan, hubungan dengan pembeli, waktu penjualan, akses ke saluran alternatif, dan tren harga.</p>
|
||||||
|
</div>
|
||||||
|
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
|
||||||
|
<i class="icon-base ti tabler-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$chartDefinitions = [
|
||||||
|
'saluranPenjualan' => ['title' => 'Saluran Penjualan Utama', 'description' => 'Kepada siapa petani biasanya menjual hasil panen?', 'variant' => 'success'],
|
||||||
|
'hubunganPembeli' => ['title' => 'Sifat Hubungan dengan Pembeli', 'description' => 'Hubungan jual petani dengan pembeli utama saat ini.', 'variant' => 'primary'],
|
||||||
|
'waktuPenjualan' => ['title' => 'Waktu Penjualan Hasil Panen', 'description' => 'Kapan petani biasanya menjual hasil panen?', 'variant' => 'warning'],
|
||||||
|
'aksesSaluranLain' => ['title' => 'Pengalaman & Akses ke Saluran Lain', 'description' => 'Pengalaman menjual ke tempat lain dan akses yang masih dimiliki.', 'variant' => 'info'],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="collapse chart-group-panel" id="saluran-harga-charts">
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
@foreach ($chartDefinitions as $key => $definition)
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="card pasar-chart-card h-100">
|
||||||
|
<div class="card-header d-flex align-items-start justify-content-between gap-2">
|
||||||
|
<div class="card-title mb-0">
|
||||||
|
<h5 class="m-0 me-2" id="title-{{ $key }}">{{ $definition['title'] }}</h5>
|
||||||
|
<small class="text-body-secondary">{{ $definition['description'] }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div id="chart-{{ $key }}" class="pasar-chart" role="img" aria-labelledby="title-{{ $key }}" aria-describedby="summary-{{ $key }}"></div>
|
||||||
|
<span id="summary-{{ $key }}" class="visually-hidden">
|
||||||
|
@foreach ($marketCharts[$key]['labels'] as $index => $label)
|
||||||
|
{{ $label }}: {{ $marketCharts[$key]['series'][$index] }} responden ({{ $marketCharts[$key]['percentages'][$index] }}%).
|
||||||
|
@endforeach
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@forelse ($priceTrend['commodities'] as $commodity)
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="card pasar-chart-card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title mb-0">
|
||||||
|
<h5 class="m-0 me-2" id="title-tren-harga-{{ $commodity['id'] }}">Tren Harga 3 Musim — {{ $commodity['name'] }}</h5>
|
||||||
|
<small class="text-body-secondary">Rata-rata harga jual per bentuk produk selama tiga periode panen (Rp/kg).</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="chart-tren-harga-{{ $commodity['id'] }}" class="pasar-chart" role="img" aria-labelledby="title-tren-harga-{{ $commodity['id'] }}" aria-describedby="summary-tren-harga-{{ $commodity['id'] }}"></div>
|
||||||
|
<span id="summary-tren-harga-{{ $commodity['id'] }}" class="visually-hidden"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card pasar-chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title mb-0"><h5 class="m-0">Tren Harga 3 Musim</h5></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pasar-chart-empty">Belum ada komoditas untuk filter yang dipilih.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card chart-group-header-toggle mb-4" role="button" tabindex="0"
|
||||||
|
data-bs-toggle="collapse" data-bs-target="#kesiapan-premium-charts"
|
||||||
|
aria-expanded="false" aria-controls="kesiapan-premium-charts" aria-label="Tampilkan chart">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<h5>Kesiapan Pasar Premium</h5>
|
||||||
|
<p class="mb-0 text-body-secondary">Kesadaran petani tentang pasar premium, persyaratan masuk, dan pengalaman mengaksesnya.</p>
|
||||||
|
</div>
|
||||||
|
<span class="btn btn-icon btn-label-secondary btn-sm chart-group-icon-toggle" aria-hidden="true">
|
||||||
|
<i class="icon-base ti tabler-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse chart-group-panel" id="kesiapan-premium-charts">
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card pasar-chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title mb-0">
|
||||||
|
<h5 class="m-0 me-2" id="title-kesadaranPremium">Kesadaran Pasar Premium</h5>
|
||||||
|
<small class="text-body-secondary">Pengetahuan tentang pasar premium dan persyaratan yang dibutuhkan untuk masuk ke pasar tersebut.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="chart-kesadaranPremium" class="pasar-chart" role="img" aria-labelledby="title-kesadaranPremium" aria-describedby="summary-kesadaranPremium"></div>
|
||||||
|
<span id="summary-kesadaranPremium" class="visually-hidden">
|
||||||
|
@foreach ($premiumAwareness['fullLabels'] as $index => $label)
|
||||||
|
{{ $label }}: {{ $premiumAwareness['series'][$index] }} responden ({{ $premiumAwareness['percentages'][$index] }}%).
|
||||||
|
@endforeach
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="mt-4" aria-labelledby="international-market-title" id="international-market">
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-2">
|
||||||
|
<h5 id="international-market-title" class="mb-0">Harga Komoditas Internasional</h5>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-market-prev aria-label="Harga sebelumnya">←</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-icon btn-outline-secondary" data-market-pause aria-pressed="false" aria-label="Jeda slider" title="Jeda slider"><i class="icon-base ti tabler-player-pause" aria-hidden="true"></i></button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-market-next aria-label="Harga berikutnya">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-body-secondary small mb-2">Setiap kartu menampilkan satu komoditas dari satu sumber. Periode dan jenis harga mengikuti penyedia; tidak mengikuti filter wilayah.</p>
|
||||||
|
<p class="small text-body-secondary" data-market-status role="status">Memuat harga internasional...</p>
|
||||||
|
<div class="international-market-track" tabindex="0" role="region" aria-roledescription="carousel" aria-label="Harga komoditas menurut sumber"></div>
|
||||||
|
<template id="international-market-card">
|
||||||
|
<article class="card card-border-shadow-info international-market-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<h6 class="mb-0" data-name></h6>
|
||||||
|
<button type="button" class="market-info-button" data-market-info data-update-info aria-label="Informasi pembaruan harga" aria-expanded="false">!</button>
|
||||||
|
</div>
|
||||||
|
<div class="small">Sumber: <a data-source target="_blank" rel="noopener noreferrer"></a></div>
|
||||||
|
</div>
|
||||||
|
<span class="international-provider-logo flex-shrink-0" data-provider-logo-wrap hidden>
|
||||||
|
<img data-provider-logo width="32" height="32" alt="" decoding="async">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pasar-kpi-value" data-price></div>
|
||||||
|
<div class="small text-body-secondary mt-1" data-unit></div>
|
||||||
|
<div class="d-flex align-items-center gap-2 mt-3 pt-3 border-top">
|
||||||
|
<div class="fw-semibold fs-5" data-rupiah></div>
|
||||||
|
<button type="button" class="market-info-button" data-market-info data-fx-info aria-label="Informasi kurs konversi" aria-expanded="false">!</button>
|
||||||
|
</div>
|
||||||
|
<p class="small text-body-secondary mt-2 mb-2" data-description></p>
|
||||||
|
<div class="small text-body-secondary" data-period></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
|
const marketSection = document.getElementById('international-market');
|
||||||
|
const marketTrack = marketSection.querySelector('.international-market-track');
|
||||||
|
const marketStatus = marketSection.querySelector('[data-market-status]');
|
||||||
|
const pauseButton = marketSection.querySelector('[data-market-pause]');
|
||||||
|
let activeMarketInfo = null;
|
||||||
|
function closeMarketInfo() {
|
||||||
|
if (!activeMarketInfo) return;
|
||||||
|
activeMarketInfo.button.setAttribute('aria-expanded', 'false');
|
||||||
|
activeMarketInfo.popover.dispose();
|
||||||
|
activeMarketInfo = null;
|
||||||
|
}
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
const button = event.target.closest('[data-market-info]');
|
||||||
|
if (button && marketSection.contains(button)) {
|
||||||
|
const same = activeMarketInfo?.button === button;
|
||||||
|
closeMarketInfo();
|
||||||
|
if (same) return;
|
||||||
|
const popover = new bootstrap.Popover(button, {
|
||||||
|
container: 'body', placement: 'auto', trigger: 'manual', html: false,
|
||||||
|
title: button.hasAttribute('data-fx-info') ? 'Kurs konversi' : 'Pembaruan harga',
|
||||||
|
content: button.dataset.info || 'Informasi belum tersedia.'
|
||||||
|
});
|
||||||
|
popover.show();
|
||||||
|
button.setAttribute('aria-expanded', 'true');
|
||||||
|
activeMarketInfo = { button, popover };
|
||||||
|
} else if (!event.target.closest('.popover')) closeMarketInfo();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
if (event.key === 'Escape') closeMarketInfo();
|
||||||
|
});
|
||||||
|
marketTrack.addEventListener('focusout', event => {
|
||||||
|
if (activeMarketInfo?.button === event.target) closeMarketInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
let marketPaused = reducedMotion.matches;
|
||||||
|
let marketSignature = '';
|
||||||
|
const quoteDateFormat = new Intl.DateTimeFormat('id-ID', {
|
||||||
|
dateStyle: 'medium', timeStyle: 'long', timeZone: 'Asia/Jakarta'
|
||||||
|
});
|
||||||
|
const rupiahFormat = new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency', currency: 'IDR', maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
function syncMarketPause() {
|
||||||
|
const label = marketPaused ? 'Putar slider' : 'Jeda slider';
|
||||||
|
pauseButton.querySelector('i').className = 'icon-base ti ' + (marketPaused ? 'tabler-player-play' : 'tabler-player-pause');
|
||||||
|
pauseButton.setAttribute('aria-label', label);
|
||||||
|
pauseButton.title = label;
|
||||||
|
pauseButton.setAttribute('aria-pressed', String(marketPaused));
|
||||||
|
}
|
||||||
|
syncMarketPause();
|
||||||
|
pauseButton.addEventListener('click', () => { marketPaused = !marketPaused; syncMarketPause(); });
|
||||||
|
let marketCycleWidth = 0;
|
||||||
|
let lastMarketFrame = null;
|
||||||
|
function measureMarketCycle() {
|
||||||
|
const first = marketTrack.firstElementChild;
|
||||||
|
const duplicate = marketTrack.querySelector('[data-market-clone]');
|
||||||
|
marketCycleWidth = first && duplicate ? duplicate.offsetLeft - first.offsetLeft : 0;
|
||||||
|
}
|
||||||
|
function slideMarket(direction) {
|
||||||
|
if (!marketTrack.firstElementChild) return;
|
||||||
|
const step = marketTrack.firstElementChild.getBoundingClientRect().width + 20;
|
||||||
|
if (direction < 0 && marketTrack.scrollLeft < step && marketCycleWidth) {
|
||||||
|
marketTrack.scrollLeft += marketCycleWidth;
|
||||||
|
}
|
||||||
|
marketTrack.scrollLeft += step * direction;
|
||||||
|
}
|
||||||
|
marketSection.querySelector('[data-market-prev]').addEventListener('click', () => slideMarket(-1));
|
||||||
|
marketSection.querySelector('[data-market-next]').addEventListener('click', () => slideMarket(1));
|
||||||
|
window.addEventListener('resize', measureMarketCycle);
|
||||||
|
function animateMarket(timestamp) {
|
||||||
|
const elapsed = lastMarketFrame === null ? 0 : Math.min(timestamp - lastMarketFrame, 64);
|
||||||
|
lastMarketFrame = timestamp;
|
||||||
|
if (marketCycleWidth > 0) {
|
||||||
|
if (!marketPaused && !document.hidden && !marketTrack.contains(document.activeElement)) {
|
||||||
|
// Keep subpixel progress independently: scrollLeft may round tiny per-frame changes.
|
||||||
|
marketScrollPosition += elapsed * 0.028;
|
||||||
|
marketTrack.scrollLeft = marketScrollPosition;
|
||||||
|
} else {
|
||||||
|
marketScrollPosition = marketTrack.scrollLeft;
|
||||||
|
}
|
||||||
|
if (marketTrack.scrollLeft >= marketCycleWidth) {
|
||||||
|
marketScrollPosition = marketTrack.scrollLeft - marketCycleWidth;
|
||||||
|
marketTrack.scrollLeft = marketScrollPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(animateMarket);
|
||||||
|
}
|
||||||
|
let marketScrollPosition = 0;
|
||||||
|
marketTrack.addEventListener('scroll', () => {
|
||||||
|
if (Math.abs(marketTrack.scrollLeft - marketScrollPosition) > 2) marketScrollPosition = marketTrack.scrollLeft;
|
||||||
|
}, { passive: true });
|
||||||
|
window.requestAnimationFrame(animateMarket);
|
||||||
|
|
||||||
|
async function refreshInternationalPrices() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(@json(route('dashboard.pasar.international')), {
|
||||||
|
headers: { 'Accept': 'application/json' }, credentials: 'same-origin',
|
||||||
|
signal: AbortSignal.timeout(25000)
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Harga tidak tersedia');
|
||||||
|
const payload = await response.json();
|
||||||
|
if (!Array.isArray(payload.quotes)) throw new Error('Data tidak valid');
|
||||||
|
const quotes = payload.quotes.filter(quote => typeof quote.price === 'number' && Number.isFinite(quote.price) && quote.price > 0);
|
||||||
|
const signature = JSON.stringify([quotes, payload.exchange_rate]);
|
||||||
|
if (signature !== marketSignature) {
|
||||||
|
const previousScroll = marketTrack.scrollLeft;
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
quotes.forEach(quote => {
|
||||||
|
const card = document.getElementById('international-market-card').content.cloneNode(true);
|
||||||
|
card.querySelector('[data-name]').textContent = quote.name;
|
||||||
|
card.querySelector('[data-update-info]').dataset.info = quote.update_info || (quote.frequency === 'monthly'
|
||||||
|
? 'IMF menyediakan harga acuan rata-rata bulanan. Data terbaru diperiksa setiap hari. Periode harga tercantum di kartu.'
|
||||||
|
: 'Harga Yahoo Finance diperiksa setiap 5 menit. Data dapat tertunda mengikuti penyedia dan jam perdagangan.');
|
||||||
|
const rate = payload.exchange_rate;
|
||||||
|
let rateInfo = 'Kurs USD/IDR belum tersedia. Harga asli tetap ditampilkan.';
|
||||||
|
if (rate && typeof rate.value === 'number') {
|
||||||
|
rateInfo = '1 USD = ' + rupiahFormat.format(rate.value) + '. Sumber: ' + rate.source + '.';
|
||||||
|
const rateDate = new Date(rate.quoted_at);
|
||||||
|
if (!Number.isNaN(rateDate.getTime())) rateInfo += ' Waktu kurs: ' + quoteDateFormat.format(rateDate) + '.';
|
||||||
|
if (rate.stale) rateInfo += ' Menggunakan kurs tersimpan.';
|
||||||
|
if (quote.frequency === 'monthly') rateInfo += ' Konversi memakai kurs ini, bukan kurs historis bulan harga tersebut.';
|
||||||
|
}
|
||||||
|
card.querySelector('[data-fx-info]').dataset.info = rateInfo;
|
||||||
|
|
||||||
|
|
||||||
|
const providerLogos = {
|
||||||
|
yahoo: @json(asset('assets/img/providers/yahoo-finance.ico')),
|
||||||
|
imf: @json(asset('assets/img/providers/imf.ico')),
|
||||||
|
alpha: @json(asset('assets/img/providers/alpha-vantage.ico'))
|
||||||
|
};
|
||||||
|
const logoUrl = providerLogos[(quote.id || '').split(':')[0]];
|
||||||
|
if (logoUrl) {
|
||||||
|
const logo = card.querySelector('[data-provider-logo]');
|
||||||
|
logo.src = logoUrl;
|
||||||
|
logo.alt = 'Logo ' + quote.source;
|
||||||
|
card.querySelector('[data-provider-logo-wrap]').hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = card.querySelector('[data-source]');
|
||||||
|
source.textContent = quote.source;
|
||||||
|
if (quote.source_url?.startsWith('https://')) source.href = quote.source_url;
|
||||||
|
card.querySelector('[data-price]').textContent = quote.price.toLocaleString('id-ID', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
card.querySelector('[data-unit]').textContent = quote.unit;
|
||||||
|
card.querySelector('[data-rupiah]').textContent = typeof quote.price_idr_per_kg === 'number'
|
||||||
|
? rupiahFormat.format(quote.price_idr_per_kg) + ' / kg' : 'Konversi rupiah belum tersedia';
|
||||||
|
card.querySelector('[data-description]').textContent = quote.description;
|
||||||
|
let period = '-';
|
||||||
|
if (quote.frequency === 'monthly' && /^\d{4}-\d{2}$/.test(quote.period || '')) {
|
||||||
|
period = 'Rata-rata ' + new Intl.DateTimeFormat('id-ID', { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(new Date(quote.period + '-01T00:00:00Z'));
|
||||||
|
} else if (quote.period) {
|
||||||
|
const date = new Date(quote.period);
|
||||||
|
if (!Number.isNaN(date.getTime())) period = 'Waktu harga: ' + quoteDateFormat.format(date);
|
||||||
|
}
|
||||||
|
card.querySelector('[data-period]').textContent = period + (quote.stale ? ' · Data tersimpan' : '');
|
||||||
|
fragment.append(card);
|
||||||
|
});
|
||||||
|
closeMarketInfo();
|
||||||
|
marketTrack.replaceChildren(fragment);
|
||||||
|
if (quotes.length > 1) {
|
||||||
|
// Repeat the cards so the end joins the start without a visible rewind.
|
||||||
|
const originals = [...marketTrack.children];
|
||||||
|
[...originals, ...originals].forEach(original => {
|
||||||
|
const clone = original.cloneNode(true);
|
||||||
|
clone.dataset.marketClone = 'true';
|
||||||
|
marketTrack.append(clone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
measureMarketCycle();
|
||||||
|
marketScrollPosition = marketCycleWidth ? previousScroll % marketCycleWidth : 0;
|
||||||
|
marketTrack.scrollLeft = marketScrollPosition;
|
||||||
|
marketSignature = signature;
|
||||||
|
}
|
||||||
|
marketStatus.textContent = quotes.length
|
||||||
|
? '' : 'Harga belum tersedia. Pembaruan akan dicoba kembali otomatis.';
|
||||||
|
marketStatus.hidden = quotes.length > 0;
|
||||||
|
[pauseButton, ...marketSection.querySelectorAll('[data-market-prev], [data-market-next]')].forEach(button => button.disabled = quotes.length < 2);
|
||||||
|
} catch (error) {
|
||||||
|
marketStatus.hidden = false;
|
||||||
|
marketStatus.textContent = marketTrack.childElementCount
|
||||||
|
? 'Pembaruan gagal. Menampilkan harga sebelumnya dan akan mencoba kembali.'
|
||||||
|
: 'Harga belum dapat dimuat. Akan dicoba kembali otomatis.';
|
||||||
|
} finally {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!document.hidden) refreshInternationalPrices();
|
||||||
|
else waitForVisible();
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function waitForVisible() {
|
||||||
|
document.addEventListener('visibilitychange', function onVisible() {
|
||||||
|
if (document.hidden) return;
|
||||||
|
document.removeEventListener('visibilitychange', onVisible);
|
||||||
|
refreshInternationalPrices();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refreshInternationalPrices();
|
||||||
|
const wilayah = @json($wilayah);
|
||||||
|
const marketCharts = @json($marketCharts);
|
||||||
|
const chartDefinitions = @json($chartDefinitions);
|
||||||
|
const priceTrend = @json($priceTrend);
|
||||||
|
const premiumAwareness = @json($premiumAwareness);
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
const element = document.createElement('span');
|
||||||
|
element.textContent = String(value);
|
||||||
|
return element.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value, digits = 0) {
|
||||||
|
return Number(value).toLocaleString('id-ID', { maximumFractionDigits: digits });
|
||||||
|
}
|
||||||
|
|
||||||
|
const compactLabels = {
|
||||||
|
saluranPenjualan: ['Tengkulak/pedagang lokal', 'Koperasi', 'BUMDes', 'Pembeli langsung/eksportir', 'Lainnya'],
|
||||||
|
waktuPenjualan: ['Langsung setelah panen', 'Ditumpuk beberapa waktu', 'Saat butuh uang mendesak'],
|
||||||
|
aksesSaluranLain: ['Tidak pernah', 'Berhenti, tanpa akses', 'Berhenti, bisa kembali', 'Sesekali masih menjual', 'Akses alternatif aktif'],
|
||||||
|
kesadaranPremium: ['Belum pernah mendengar', 'Belum paham persyaratan', 'Sedikit paham persyaratan', 'Paham, belum mengakses', 'Akses premium konsisten'],
|
||||||
|
};
|
||||||
|
const mobileLabels = {
|
||||||
|
saluranPenjualan: ['Tengkulak/lokal', 'Koperasi', 'BUMDes', 'Pembeli/eksportir', 'Lainnya'],
|
||||||
|
hubunganPembeli: ['Bebas pindah', 'Langganan tetap', 'Berbasis hutang', 'Titip jual', 'Komitmen volume', 'Semi-kontrak'],
|
||||||
|
waktuPenjualan: ['Setelah panen', 'Ditumpuk dulu', 'Uang mendesak'],
|
||||||
|
aksesSaluranLain: ['Tidak pernah', 'Tanpa akses', 'Bisa kembali', 'Sesekali jual', 'Akses aktif'],
|
||||||
|
kesadaranPremium: ['Belum mendengar', 'Belum paham syarat', 'Sedikit paham', 'Paham, belum akses', 'Akses konsisten'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasTrendData(commodity) {
|
||||||
|
return commodity && commodity.series.some(function (series) {
|
||||||
|
return series.data.some(function (value) { return value !== null && Number(value) > 0; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(value) {
|
||||||
|
return value === null || value === undefined ? 'Belum ada data' : 'Rp ' + formatNumber(value) + '/kg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPriceTrend(commodity) {
|
||||||
|
const element = document.getElementById('chart-tren-harga-' + commodity.id);
|
||||||
|
const summary = document.getElementById('summary-tren-harga-' + commodity.id);
|
||||||
|
|
||||||
|
if (!hasTrendData(commodity) || typeof ApexCharts === 'undefined') {
|
||||||
|
element.classList.add('pasar-chart-empty');
|
||||||
|
element.textContent = !hasTrendData(commodity)
|
||||||
|
? 'Belum ada data harga untuk komoditas dan filter yang dipilih.'
|
||||||
|
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.textContent = commodity.name + '. ' + commodity.series.map(function (series) {
|
||||||
|
return series.name + ': ' + series.data.map(function (value, index) {
|
||||||
|
return priceTrend.periods[index] + ' ' + formatPrice(value);
|
||||||
|
}).join(', ');
|
||||||
|
}).join('. ');
|
||||||
|
|
||||||
|
const colors = config.chartColors.series.slice(0, 4);
|
||||||
|
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
|
||||||
|
new ApexCharts(element, {
|
||||||
|
chart: { type: 'line', height: 360, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false }, zoom: { enabled: false } },
|
||||||
|
series: commodity.series,
|
||||||
|
colors: colors,
|
||||||
|
stroke: { width: 3, curve: 'straight' },
|
||||||
|
markers: { size: 5, strokeWidth: 2, hover: { sizeOffset: 2 } },
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
xaxis: {
|
||||||
|
categories: priceTrend.periods,
|
||||||
|
tickPlacement: 'between',
|
||||||
|
labels: { rotate: 0, style: labelStyle },
|
||||||
|
axisBorder: { color: config.chartColors.grid },
|
||||||
|
axisTicks: { color: config.chartColors.grid },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: 0,
|
||||||
|
title: { text: 'Harga (Rp/kg)', style: { color: config.colors.bodyColor, fontFamily: config.fontFamily, fontWeight: 400 } },
|
||||||
|
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
|
||||||
|
},
|
||||||
|
grid: { borderColor: config.chartColors.grid, strokeDashArray: 4, padding: { left: 8, right: 20 } },
|
||||||
|
legend: { position: 'bottom', fontSize: '13px', fontFamily: config.fontFamily, labels: { colors: config.colors.bodyColor }, itemMargin: { horizontal: 12, vertical: 6 } },
|
||||||
|
tooltip: {
|
||||||
|
shared: true,
|
||||||
|
intersect: false,
|
||||||
|
custom: function ({ dataPointIndex }) {
|
||||||
|
const rows = commodity.series.map(function (series, index) {
|
||||||
|
return `<div class="pasar-trend-tooltip-row"><span style="color:${colors[index]}">${escapeHtml(series.name)}</span><strong>${formatPrice(series.data[dataPointIndex])}</strong></div>`;
|
||||||
|
}).join('');
|
||||||
|
return `<div class="pasar-chart-tooltip" style="background:${config.colors.cardColor};color:${config.colors.headingColor}"><strong>${escapeHtml(commodity.name)} — ${escapeHtml(priceTrend.periods[dataPointIndex])}</strong>${rows}</div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responsive: [{
|
||||||
|
breakpoint: 576,
|
||||||
|
options: {
|
||||||
|
chart: { height: 420 },
|
||||||
|
legend: { fontSize: '12px', itemMargin: { horizontal: 6, vertical: 4 } },
|
||||||
|
xaxis: {
|
||||||
|
labels: {
|
||||||
|
hideOverlappingLabels: false,
|
||||||
|
style: { fontSize: '11px' },
|
||||||
|
formatter: function (value) {
|
||||||
|
return { 'Dua Tahun Lalu': '2 thn lalu', 'Tahun Lalu': 'Tahun lalu', 'Panen Terakhir': 'Terakhir' }[value] || value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: { text: undefined },
|
||||||
|
labels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResponseChart(key, data, definition) {
|
||||||
|
const element = document.getElementById('chart-' + key);
|
||||||
|
if (!data.respondents || typeof ApexCharts === 'undefined') {
|
||||||
|
element.classList.add('pasar-chart-empty');
|
||||||
|
element.textContent = !data.respondents
|
||||||
|
? 'Belum ada data jawaban untuk filter yang dipilih.'
|
||||||
|
: 'Chart belum dapat dimuat. Silakan muat ulang halaman.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = config.chartColors.semantic[definition.variant];
|
||||||
|
const borderColor = config.chartColors.grid;
|
||||||
|
const labelStyle = { colors: config.colors.bodyColor, fontSize: '12px', fontFamily: config.fontFamily };
|
||||||
|
const maxCount = Math.max(...data.series);
|
||||||
|
const tickCount = Math.min(4, maxCount);
|
||||||
|
const axisMax = Math.ceil(maxCount * 1.25 / tickCount) * tickCount;
|
||||||
|
|
||||||
|
new ApexCharts(element, {
|
||||||
|
chart: { type: 'bar', height: 330, fontFamily: config.fontFamily, foreColor: config.colors.bodyColor, toolbar: { show: false } },
|
||||||
|
series: [{ name: 'Responden', data: data.series }],
|
||||||
|
colors: [color],
|
||||||
|
plotOptions: { bar: { horizontal: true, borderRadius: 5, barHeight: '62%', dataLabels: { position: 'top' } } },
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
textAnchor: 'start',
|
||||||
|
offsetX: 6,
|
||||||
|
style: { fontSize: '12px', fontFamily: config.fontFamily, fontWeight: 500, colors: [config.colors.headingColor] },
|
||||||
|
formatter: function (value, options) {
|
||||||
|
return `${formatNumber(value)} (${formatNumber(data.percentages[options.dataPointIndex], 1)}%)`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: compactLabels[key] || data.labels,
|
||||||
|
min: 0,
|
||||||
|
max: axisMax,
|
||||||
|
tickAmount: tickCount,
|
||||||
|
labels: { style: labelStyle, formatter: function (value) { return formatNumber(value); } },
|
||||||
|
axisBorder: { color: borderColor },
|
||||||
|
axisTicks: { color: borderColor },
|
||||||
|
},
|
||||||
|
yaxis: { labels: { maxWidth: 185, style: labelStyle } },
|
||||||
|
grid: { borderColor: borderColor, strokeDashArray: 4, padding: { right: 20 } },
|
||||||
|
legend: { show: false },
|
||||||
|
tooltip: {
|
||||||
|
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light',
|
||||||
|
custom: function ({ dataPointIndex }) {
|
||||||
|
return `<div class="pasar-chart-tooltip" style="background:${color};color:${config.colors.headingColor};font-family:${config.fontFamily}">${escapeHtml(data.fullLabels[dataPointIndex])}: <strong>${formatNumber(data.series[dataPointIndex])} responden (${formatNumber(data.percentages[dataPointIndex], 1)}%)</strong></div>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responsive: [{
|
||||||
|
breakpoint: 576,
|
||||||
|
options: {
|
||||||
|
xaxis: { categories: mobileLabels[key], tickAmount: 2 },
|
||||||
|
yaxis: { labels: { maxWidth: 125, style: { fontSize: '11px' } } },
|
||||||
|
grid: { padding: { right: 10 } },
|
||||||
|
dataLabels: { style: { fontSize: '11px' }, formatter: function (value) { return formatNumber(value); } },
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}).render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarketCharts() {
|
||||||
|
Object.entries(marketCharts).forEach(function ([key, data]) {
|
||||||
|
renderResponseChart(key, data, chartDefinitions[key]);
|
||||||
|
});
|
||||||
|
priceTrend.commodities.forEach(renderPriceTrend);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupChartPanel(panelId, renderCharts) {
|
||||||
|
const chartPanel = document.getElementById(panelId);
|
||||||
|
const chartControl = document.querySelector('.chart-group-header-toggle[aria-controls="' + chartPanel.id + '"]');
|
||||||
|
const chartIcon = chartControl.querySelector('.chart-group-icon-toggle i');
|
||||||
|
let rendered = false;
|
||||||
|
|
||||||
|
function syncChartPanelState() {
|
||||||
|
const expanded = chartPanel.classList.contains('show');
|
||||||
|
const label = expanded ? 'Sembunyikan chart' : 'Tampilkan chart';
|
||||||
|
chartControl.setAttribute('aria-expanded', String(expanded));
|
||||||
|
chartControl.setAttribute('aria-label', label);
|
||||||
|
chartControl.title = label;
|
||||||
|
chartIcon.classList.toggle('tabler-chevron-up', expanded);
|
||||||
|
chartIcon.classList.toggle('tabler-chevron-down', !expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartControl.addEventListener('keydown', function (event) {
|
||||||
|
if (event.target !== chartControl || !['Enter', ' '].includes(event.key)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
chartControl.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
chartPanel.addEventListener('shown.bs.collapse', function (event) {
|
||||||
|
if (event.target !== chartPanel) return;
|
||||||
|
syncChartPanelState();
|
||||||
|
if (!rendered) {
|
||||||
|
renderCharts();
|
||||||
|
rendered = true;
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
|
||||||
|
chartPanel.addEventListener('hidden.bs.collapse', function (event) {
|
||||||
|
if (event.target === chartPanel) syncChartPanelState();
|
||||||
|
});
|
||||||
|
|
||||||
|
syncChartPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupChartPanel('saluran-harga-charts', renderMarketCharts);
|
||||||
|
setupChartPanel('kesiapan-premium-charts', function () {
|
||||||
|
renderResponseChart('kesadaranPremium', premiumAwareness, { variant: 'primary' });
|
||||||
|
});
|
||||||
|
|
||||||
|
function fillSelect($select, items, placeholder) {
|
||||||
|
$select.empty().append(new Option(placeholder, ''));
|
||||||
|
items.forEach(function (item) {
|
||||||
|
$select.append(new Option(item.nama, item.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedProvinsi() {
|
||||||
|
const id = $('#filter-provinsi').val();
|
||||||
|
return wilayah.find(function (item) {
|
||||||
|
return String(item.id) === String(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedKabupaten() {
|
||||||
|
const provinsi = selectedProvinsi();
|
||||||
|
const id = $('#filter-kabkota').val();
|
||||||
|
return (provinsi?.kabupaten || []).find(function (item) {
|
||||||
|
return String(item.id) === String(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedKecamatan() {
|
||||||
|
const kabupaten = selectedKabupaten();
|
||||||
|
const id = $('#filter-kecamatan').val();
|
||||||
|
return (kabupaten?.kecamatan || []).find(function (item) {
|
||||||
|
return String(item.id) === String(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#filter-provinsi, #filter-kabkota, #filter-kecamatan, #filter-desa, #filter-komoditas, #filter-segmen').select2({ width: '100%' });
|
||||||
|
|
||||||
|
$('#filter-provinsi').on('change', function () {
|
||||||
|
const provinsi = selectedProvinsi();
|
||||||
|
fillSelect($('#filter-kabkota'), provinsi?.kabupaten || [], 'Semua Kabupaten/Kota');
|
||||||
|
fillSelect($('#filter-kecamatan'), [], 'Semua Kecamatan');
|
||||||
|
fillSelect($('#filter-desa'), [], 'Semua Desa/Kelurahan');
|
||||||
|
$('#filter-kabkota').prop('disabled', !provinsi).trigger('change.select2');
|
||||||
|
$('#filter-kecamatan, #filter-desa').prop('disabled', true).trigger('change.select2');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#filter-kabkota').on('change', function () {
|
||||||
|
const kabupaten = selectedKabupaten();
|
||||||
|
fillSelect($('#filter-kecamatan'), kabupaten?.kecamatan || [], 'Semua Kecamatan');
|
||||||
|
fillSelect($('#filter-desa'), [], 'Semua Desa/Kelurahan');
|
||||||
|
$('#filter-kecamatan').prop('disabled', !kabupaten).trigger('change.select2');
|
||||||
|
$('#filter-desa').prop('disabled', true).trigger('change.select2');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#filter-kecamatan').on('change', function () {
|
||||||
|
const kecamatan = selectedKecamatan();
|
||||||
|
fillSelect($('#filter-desa'), kecamatan?.desa || [], 'Semua Desa/Kelurahan');
|
||||||
|
$('#filter-desa').prop('disabled', !kecamatan).trigger('change.select2');
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProvinsi = String($('#filter-provinsi').data('default') || '');
|
||||||
|
const defaultKabupaten = String($('#filter-kabkota').data('default') || '');
|
||||||
|
const defaultKecamatan = String($('#filter-kecamatan').data('default') || '');
|
||||||
|
const defaultDesa = String($('#filter-desa').data('default') || '');
|
||||||
|
|
||||||
|
if (defaultProvinsi) {
|
||||||
|
$('#filter-provinsi').val(defaultProvinsi).trigger('change');
|
||||||
|
if (defaultKabupaten) {
|
||||||
|
$('#filter-kabkota').val(defaultKabupaten).trigger('change');
|
||||||
|
if (defaultKecamatan) {
|
||||||
|
$('#filter-kecamatan').val(defaultKecamatan).trigger('change');
|
||||||
|
if (defaultDesa) {
|
||||||
|
$('#filter-desa').val(defaultDesa).trigger('change.select2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#btn-reset-dashboard-filter').on('click', function () {
|
||||||
|
window.location.href = @json(route('dashboard.pasar'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -493,6 +493,7 @@ $segments = [
|
|||||||
legendColor = config.colors.bodyColor;
|
legendColor = config.colors.bodyColor;
|
||||||
borderColor = config.colors.borderColor;
|
borderColor = config.colors.borderColor;
|
||||||
headingColor = config.colors.headingColor;
|
headingColor = config.colors.headingColor;
|
||||||
|
const pastelSemantic = config.chartColors?.semantic || {};
|
||||||
|
|
||||||
var radarDimensi = new ApexCharts($("#chart-dimensi")[0],
|
var radarDimensi = new ApexCharts($("#chart-dimensi")[0],
|
||||||
options = {
|
options = {
|
||||||
@@ -542,7 +543,7 @@ $segments = [
|
|||||||
highlightDataSeries: false
|
highlightDataSeries: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: [config.colors.primary, config.colors.info],
|
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
|
||||||
fill: {
|
fill: {
|
||||||
opacity: 0.85
|
opacity: 0.85
|
||||||
},
|
},
|
||||||
@@ -640,7 +641,7 @@ $segments = [
|
|||||||
highlightDataSeries: false
|
highlightDataSeries: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: [config.colors.primary, config.colors.info],
|
colors: [pastelSemantic.primary || '#9D8EF0', pastelSemantic.info || '#5FC7D6'],
|
||||||
fill: {
|
fill: {
|
||||||
opacity: 0.85
|
opacity: 0.85
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -144,10 +144,10 @@
|
|||||||
<div class="absolute bottom-3 left-3 bg-white/95 p-3 rounded-lg text-xs border border-gray-200 shadow-sm backdrop-blur-sm">
|
<div class="absolute bottom-3 left-3 bg-white/95 p-3 rounded-lg text-xs border border-gray-200 shadow-sm backdrop-blur-sm">
|
||||||
<div class="font-bold text-gray-700 mb-2">Legenda Tanaman:</div>
|
<div class="font-bold text-gray-700 mb-2">Legenda Tanaman:</div>
|
||||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||||
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6F4E37] rounded-full shadow-sm"></div> Kopi</div>
|
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#6FD68F] rounded-full shadow-sm"></div> Kopi</div>
|
||||||
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#eab308] rounded-full shadow-sm"></div> Vanili</div>
|
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#FFB46F] rounded-full shadow-sm"></div> Vanili</div>
|
||||||
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#D2691E] rounded-full shadow-sm"></div> Kakao</div>
|
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9D8EF0] rounded-full shadow-sm"></div> Kakao</div>
|
||||||
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9ca3af] rounded-full shadow-sm"></div> Kemiri</div>
|
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[#9EA6B0] rounded-full shadow-sm"></div> Kemiri</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +253,9 @@
|
|||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelCategory = pastelChart.categories || {};
|
||||||
|
const gridColor = pastelChart.grid || '#E8EDF5';
|
||||||
|
|
||||||
// 1. Inisialisasi Icons
|
// 1. Inisialisasi Icons
|
||||||
if(typeof lucide !== 'undefined') {
|
if(typeof lucide !== 'undefined') {
|
||||||
@@ -263,19 +266,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const spatialData = [
|
const spatialData = [
|
||||||
{
|
{
|
||||||
id: 'L-001', owner: 'Yosef Neta', familySize: 5, area: '1.2 Ha', village: 'Radabata',
|
id: 'L-001', owner: 'Yosef Neta', familySize: 5, area: '1.2 Ha', village: 'Radabata',
|
||||||
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(34, 197, 94, 0.15)", stroke: "#16a34a",
|
polygon: "15,25 75,15 90,85 20,70", fill: "rgba(111, 214, 143, 0.24)", stroke: "#6FD68F",
|
||||||
plants: [
|
plants: [
|
||||||
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: '#6F4E37' },
|
{ id: 'P-01', type: 'Kopi', cx: 35, cy: 35, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemupukan Organik (Okt 2023)', panen: '45 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
|
||||||
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: '#eab308' },
|
{ id: 'P-02', type: 'Vanili', cx: 65, cy: 45, planted: 'Jan 2022', status: 'Berbunga', intervensi: 'Pelatihan Polinasi (Feb 2024)', panen: 'Belum panen', color: pastelCategory.vanilla || '#FFB46F' },
|
||||||
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: '#6F4E37' },
|
{ id: 'P-03', type: 'Kopi', cx: 50, cy: 65, planted: 'Mei 2021', status: 'Produktif', intervensi: 'Pemangkasan (Nov 2023)', panen: '40 kg/musim', color: pastelCategory.coffee || '#6FD68F' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'L-002', owner: 'Maria Goreti', familySize: 3, area: '0.8 Ha', village: 'Ubedolumolo',
|
id: 'L-002', owner: 'Maria Goreti', familySize: 3, area: '0.8 Ha', village: 'Ubedolumolo',
|
||||||
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(59, 130, 246, 0.15)", stroke: "#2563eb",
|
polygon: "110,40 170,15 190,65 125,85", fill: "rgba(95, 199, 214, 0.20)", stroke: "#5FC7D6",
|
||||||
plants: [
|
plants: [
|
||||||
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: '#9ca3af' },
|
{ id: 'P-04', type: 'Kemiri', cx: 140, cy: 45, planted: 'Agu 2018', status: 'Produktif', intervensi: 'Pembersihan Gulma', panen: '120 kg/musim', color: pastelCategory.candlenut || '#9EA6B0' },
|
||||||
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: '#D2691E' },
|
{ id: 'P-05', type: 'Kakao', cx: 165, cy: 55, planted: 'Okt 2020', status: 'Terserang Hama', intervensi: 'Penyemprotan Bio-pestisida (Jan 2024)', panen: '20 kg/musim', color: pastelCategory.cacao || '#9D8EF0' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -440,24 +443,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
{
|
{
|
||||||
label: 'Kopi',
|
label: 'Kopi',
|
||||||
data: [0.5, 0.7, 1.1, 1.5],
|
data: [0.5, 0.7, 1.1, 1.5],
|
||||||
borderColor: '#6F4E37',
|
borderColor: pastelCategory.coffee || '#6FD68F',
|
||||||
backgroundColor: '#6F4E37',
|
backgroundColor: pastelCategory.coffee || '#6FD68F',
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Vanili',
|
label: 'Vanili',
|
||||||
data: [0.1, 0.15, 0.3, 0.5],
|
data: [0.1, 0.15, 0.3, 0.5],
|
||||||
borderColor: '#eab308',
|
borderColor: pastelCategory.vanilla || '#FFB46F',
|
||||||
backgroundColor: '#eab308',
|
backgroundColor: pastelCategory.vanilla || '#FFB46F',
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kakao',
|
label: 'Kakao',
|
||||||
data: [0.4, 0.5, 0.8, 1.2],
|
data: [0.4, 0.5, 0.8, 1.2],
|
||||||
borderColor: '#D2691E',
|
borderColor: pastelCategory.cacao || '#9D8EF0',
|
||||||
backgroundColor: '#D2691E',
|
backgroundColor: pastelCategory.cacao || '#9D8EF0',
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
}
|
}
|
||||||
@@ -467,7 +470,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
interaction: { mode: 'index', intersect: false },
|
interaction: { mode: 'index', intersect: false },
|
||||||
scales: {
|
scales: {
|
||||||
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: '#f3f4f6' } },
|
y: { title: { display: true, text: 'Ton per Hektar (Ton/Ha)' }, grid: { color: gridColor } },
|
||||||
x: { grid: { display: false } }
|
x: { grid: { display: false } }
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -489,11 +492,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
label: 'Jumlah Bibit Didistribusikan',
|
label: 'Jumlah Bibit Didistribusikan',
|
||||||
data: [15000, 5200, 8000, 7000, 10000],
|
data: [15000, 5200, 8000, 7000, 10000],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(111, 78, 55, 0.8)', // Kopi
|
'rgba(111, 214, 143, 0.86)', // Kopi
|
||||||
'rgba(234, 179, 8, 0.8)', // Vanili
|
'rgba(255, 180, 111, 0.86)', // Vanili
|
||||||
'rgba(210, 105, 30, 0.8)', // Kakao
|
'rgba(157, 142, 240, 0.86)', // Kakao
|
||||||
'rgba(156, 163, 175, 0.8)', // Kemiri
|
'rgba(179, 181, 188, 0.86)', // Kemiri
|
||||||
'rgba(34, 197, 94, 0.8)' // Pelindung (Hijau)
|
'rgba(101, 216, 189, 0.86)' // Pelindung
|
||||||
],
|
],
|
||||||
borderRadius: 4
|
borderRadius: 4
|
||||||
}
|
}
|
||||||
@@ -502,7 +505,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
y: { beginAtZero: true, grid: { color: '#f3f4f6' } },
|
y: { beginAtZero: true, grid: { color: gridColor } },
|
||||||
x: { grid: { display: false } }
|
x: { grid: { display: false } }
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
@@ -219,6 +219,10 @@
|
|||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const pastelChart = config.chartColors || {};
|
||||||
|
const pastelSemantic = pastelChart.semantic || {};
|
||||||
|
const pastelCategory = pastelChart.categories || {};
|
||||||
|
const gridColor = pastelChart.grid || '#E8EDF5';
|
||||||
|
|
||||||
// Inisialisasi Icons (Khusus Mockup, Vuexy mungkin pakai library icon sendiri seperti Tabler/FontAwesome)
|
// Inisialisasi Icons (Khusus Mockup, Vuexy mungkin pakai library icon sendiri seperti Tabler/FontAwesome)
|
||||||
if(typeof lucide !== 'undefined') {
|
if(typeof lucide !== 'undefined') {
|
||||||
@@ -236,7 +240,7 @@
|
|||||||
{
|
{
|
||||||
label: 'Baseline (Tanpa Intervensi)',
|
label: 'Baseline (Tanpa Intervensi)',
|
||||||
data: [12, 12.5, 13, 13.5],
|
data: [12, 12.5, 13, 13.5],
|
||||||
borderColor: '#9ca3af',
|
borderColor: pastelSemantic.gray || '#9EA6B0',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
fill: false,
|
fill: false,
|
||||||
@@ -245,11 +249,11 @@
|
|||||||
{
|
{
|
||||||
label: 'Pendapatan Aktual',
|
label: 'Pendapatan Aktual',
|
||||||
data: [12, 15, 22, 31],
|
data: [12, 15, 22, 31],
|
||||||
borderColor: '#10b981',
|
borderColor: pastelCategory.marketGreen || '#6FD68F',
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.15)', // Light green fill
|
backgroundColor: 'rgba(111, 214, 143, 0.20)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointBackgroundColor: '#10b981',
|
pointBackgroundColor: pastelCategory.marketGreen || '#6FD68F',
|
||||||
pointBorderWidth: 2,
|
pointBorderWidth: 2,
|
||||||
pointRadius: 4
|
pointRadius: 4
|
||||||
}
|
}
|
||||||
@@ -265,7 +269,7 @@
|
|||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: false,
|
beginAtZero: false,
|
||||||
grid: { color: '#f3f4f6' },
|
grid: { color: gridColor },
|
||||||
ticks: { callback: function(val) { return 'Rp ' + val + 'M'; } }
|
ticks: { callback: function(val) { return 'Rp ' + val + 'M'; } }
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
@@ -296,7 +300,12 @@
|
|||||||
labels: ['Kopi', 'Vanili', 'Kakao', 'Kemiri'],
|
labels: ['Kopi', 'Vanili', 'Kakao', 'Kemiri'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [45, 25, 15, 15],
|
data: [45, 25, 15, 15],
|
||||||
backgroundColor: ['#6F4E37', '#eab308', '#D2691E', '#9ca3af'],
|
backgroundColor: [
|
||||||
|
pastelCategory.coffee || '#6FD68F',
|
||||||
|
pastelCategory.vanilla || '#FFB46F',
|
||||||
|
pastelCategory.cacao || '#9D8EF0',
|
||||||
|
pastelCategory.candlenut || '#9EA6B0'
|
||||||
|
],
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ffffff',
|
borderColor: '#ffffff',
|
||||||
hoverOffset: 4
|
hoverOffset: 4
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
box-shadow: 0 .25rem 1rem rgba(34, 48, 62, .08);
|
box-shadow: 0 .25rem 1rem rgba(34, 48, 62, .08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tk-stat.green { border-left-color: #28a745; }
|
.tk-stat.green { border-left-color: #6FD68F; }
|
||||||
.tk-stat.blue { border-left-color: #007bff; }
|
.tk-stat.blue { border-left-color: #5FC7D6; }
|
||||||
.tk-stat.amber { border-left-color: #fd7e14; }
|
.tk-stat.amber { border-left-color: #FFB46F; }
|
||||||
|
|
||||||
.tk-stat-lbl {
|
.tk-stat-lbl {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -444,10 +444,10 @@
|
|||||||
@forelse ($progressPetani as $petani)
|
@forelse ($progressPetani as $petani)
|
||||||
@php
|
@php
|
||||||
$barColor = match (true) {
|
$barColor = match (true) {
|
||||||
$petani['percent'] === 100 => '#28a745',
|
$petani['percent'] === 100 => '#6FD68F',
|
||||||
$petani['percent'] >= 50 => '#007bff',
|
$petani['percent'] >= 50 => '#5FC7D6',
|
||||||
$petani['percent'] >= 20 => '#fd7e14',
|
$petani['percent'] >= 20 => '#FFB46F',
|
||||||
default => '#dc3545',
|
default => '#F47C86',
|
||||||
};
|
};
|
||||||
|
|
||||||
$initial = collect(explode(' ', trim($petani['name'])))
|
$initial = collect(explode(' ', trim($petani['name'])))
|
||||||
@@ -465,8 +465,8 @@
|
|||||||
style="
|
style="
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background: rgba(40,167,69,.15);
|
background: rgba(111,214,143,.24);
|
||||||
color: #28a745;
|
color: #6FD68F;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ $initial ?: 'PT' }}
|
{{ $initial ?: 'PT' }}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<option value="">Pilih Role</option>
|
<option value="">Pilih Role</option>
|
||||||
@foreach($roles as $roleEnum)
|
@foreach($roles as $roleEnum)
|
||||||
<option value="{{ $roleEnum->value }}" {{ old('role', \App\Enums\Role::PETANI->value) === $roleEnum->value ? 'selected' : '' }}>
|
<option value="{{ $roleEnum->value }}" {{ old('role', \App\Enums\Role::PETANI->value) === $roleEnum->value ? 'selected' : '' }}>
|
||||||
{{ ucfirst(str_replace('_', ' ', $roleEnum->value)) }}
|
{{ $roleEnum->label() }}
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@@ -55,6 +55,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Hanya untuk role Koordinator --}}
|
||||||
|
<div class="row {{ old('role') === \App\Enums\Role::KOORDINATOR->value ? '' : 'd-none' }}" id="koordinator-komoditas-wrapper">
|
||||||
|
<div class="col-md-12 mb-4 form-control-validation">
|
||||||
|
<label class="form-label" for="add-koordinator-komoditas">Komoditas yang Dikoordinasikan<span class="text-danger">*</span></label>
|
||||||
|
<select id="add-koordinator-komoditas" name="komoditas_ids[]" class="select2 form-select @error('komoditas_ids') is-invalid @enderror" multiple data-placeholder="Pilih Komoditas" style="width: 100%">
|
||||||
|
@foreach($komoditasOptions as $komoditas)
|
||||||
|
<option value="{{ $komoditas->id }}" @selected(in_array($komoditas->id, old('komoditas_ids', [])))>{{ $komoditas->nama }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('komoditas_ids')<div class="invalid-feedback d-block">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-4 form-control-validation">
|
<div class="col-md-6 mb-4 form-control-validation">
|
||||||
<label class="form-label" for="add-profile-telepon">No. Telepon<span class="text-danger">*</span></label>
|
<label class="form-label" for="add-profile-telepon">No. Telepon<span class="text-danger">*</span></label>
|
||||||
@@ -183,6 +196,12 @@
|
|||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('.select2').select2();
|
$('.select2').select2();
|
||||||
|
|
||||||
|
// Pilihan komoditas hanya tampil untuk role Koordinator
|
||||||
|
$('#add-profile-role').on('change', function () {
|
||||||
|
const isKoordinator = $(this).val() === @json(\App\Enums\Role::KOORDINATOR->value);
|
||||||
|
$('#koordinator-komoditas-wrapper').toggleClass('d-none', !isKoordinator);
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.password-toggle', function () {
|
$(document).on('click', '.password-toggle', function () {
|
||||||
const input = this.closest('.input-group').querySelector('input');
|
const input = this.closest('.input-group').querySelector('input');
|
||||||
const icon = this.querySelector('i');
|
const icon = this.querySelector('i');
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<option value="">-- All Roles --</option>
|
<option value="">-- All Roles --</option>
|
||||||
@foreach($roles as $roleEnum)
|
@foreach($roles as $roleEnum)
|
||||||
<option value="{{ $roleEnum->value }}" {{ request('role') == $roleEnum->value ? 'selected' : '' }}>
|
<option value="{{ $roleEnum->value }}" {{ request('role') == $roleEnum->value ? 'selected' : '' }}>
|
||||||
{{ ucfirst(str_replace('_', ' ', $roleEnum->value)) }}
|
{{ $roleEnum->label() }}
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@@ -164,14 +164,24 @@
|
|||||||
$roleColors = [
|
$roleColors = [
|
||||||
'super_admin' => 'bg-label-danger',
|
'super_admin' => 'bg-label-danger',
|
||||||
'admin' => 'bg-label-warning',
|
'admin' => 'bg-label-warning',
|
||||||
'fasilitator' => 'bg-label-info',
|
'koordinator' => 'bg-label-primary',
|
||||||
|
'enumerator' => 'bg-label-info',
|
||||||
|
'fasilitator' => 'bg-label-dark',
|
||||||
'petani' => 'bg-label-success',
|
'petani' => 'bg-label-success',
|
||||||
];
|
];
|
||||||
$color = $roleColors[$roleKey] ?? 'bg-label-secondary';
|
$color = $roleColors[$roleKey] ?? 'bg-label-secondary';
|
||||||
|
$roleLabel = ($user->role instanceof \App\Enums\Role)
|
||||||
|
? $user->role->label()
|
||||||
|
: ucfirst(str_replace('_', ' ', (string) $roleKey));
|
||||||
@endphp
|
@endphp
|
||||||
<span class="badge {{ $color }}">
|
<span class="badge {{ $color }}">
|
||||||
{{ ucfirst(str_replace('_', ' ', $roleKey)) }}
|
{{ $roleLabel }}
|
||||||
</span>
|
</span>
|
||||||
|
@if($roleKey === 'koordinator')
|
||||||
|
<small class="d-block text-muted mt-1">
|
||||||
|
{{ $user->koordinatorKomoditas->pluck('komoditas.nama')->filter()->join(', ') ?: 'Belum ada komoditas' }}
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if($user->email_verified_at)
|
@if($user->email_verified_at)
|
||||||
@@ -192,6 +202,16 @@
|
|||||||
<i class="ti tabler-trash ti-md"></i>
|
<i class="ti tabler-trash ti-md"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@if($roleKey === 'koordinator')
|
||||||
|
<a href="javascript:;" class="btn btn-icon btn-text-secondary rounded-pill btn-komoditas-koordinator"
|
||||||
|
title="Atur Komoditas"
|
||||||
|
data-url="{{ route('users.komoditas.update', $user->id) }}"
|
||||||
|
data-name="{{ $user->name }}"
|
||||||
|
data-selected='@json($user->koordinatorKomoditas->pluck('komoditas_id')->values())'>
|
||||||
|
<i class="ti tabler-plant ti-md"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if(!$user->email_verified_at)
|
@if(!$user->email_verified_at)
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-primary px-9 d-flex align-items-center btn-verify shadow-sm waves-effect waves-light"
|
class="btn btn-primary px-9 d-flex align-items-center btn-verify shadow-sm waves-effect waves-light"
|
||||||
@@ -241,7 +261,9 @@
|
|||||||
<option value="" selected disabled>-- Pilih Role Akses --</option>
|
<option value="" selected disabled>-- Pilih Role Akses --</option>
|
||||||
<option value="super_admin">🟣 Super Admin</option>
|
<option value="super_admin">🟣 Super Admin</option>
|
||||||
<option value="admin">🟣 Admin</option>
|
<option value="admin">🟣 Admin</option>
|
||||||
<option value="fasilitator">🟣 Fasilitator</option>
|
<option value="koordinator">🟣 Koordinator</option>
|
||||||
|
<option value="enumerator">🟣 Enumerator</option>
|
||||||
|
<option value="fasilitator">🟣 Fasilitator (Pemateri Workshop)</option>
|
||||||
<option value="offtaker">🟣 Offtaker</option>
|
<option value="offtaker">🟣 Offtaker</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,4 +417,67 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
// Atur komoditas yang ditangani koordinator
|
||||||
|
$(function () {
|
||||||
|
const komoditasOptions = @json($komoditasOptions);
|
||||||
|
|
||||||
|
$(document).on('click', '.btn-komoditas-koordinator', function () {
|
||||||
|
const url = $(this).data('url');
|
||||||
|
const nama = $(this).data('name');
|
||||||
|
const selected = ($(this).data('selected') || []).map(Number);
|
||||||
|
|
||||||
|
const checkboxes = komoditasOptions.map(function (k) {
|
||||||
|
const checked = selected.includes(k.id) ? 'checked' : '';
|
||||||
|
return `<div class="form-check text-start mb-2">
|
||||||
|
<input class="form-check-input komoditas-koordinator-check" type="checkbox" value="${k.id}" id="kk-${k.id}" ${checked}>
|
||||||
|
<label class="form-check-label" for="kk-${k.id}">${$('<div>').text(k.nama).html()}</label>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Komoditas Koordinator',
|
||||||
|
html: `<p class="text-muted mb-4">Pilih komoditas yang dikoordinasikan oleh <strong>${$('<div>').text(nama).html()}</strong>.</p>
|
||||||
|
<div class="px-4">${checkboxes || '<p class="text-muted">Belum ada komoditas utama.</p>'}</div>`,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '<i class="ti tabler-check me-1"></i> Simpan',
|
||||||
|
cancelButtonText: 'Batal',
|
||||||
|
customClass: {
|
||||||
|
popup: 'rounded-4',
|
||||||
|
confirmButton: 'btn btn-primary me-3',
|
||||||
|
cancelButton: 'btn btn-label-secondary'
|
||||||
|
},
|
||||||
|
buttonsStyling: false,
|
||||||
|
preConfirm: function () {
|
||||||
|
const ids = Array.from(Swal.getPopup().querySelectorAll('.komoditas-koordinator-check:checked')).map(el => el.value);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
Swal.showValidationMessage('Pilih minimal satu komoditas.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'PUT',
|
||||||
|
headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' },
|
||||||
|
data: { komoditas_ids: result.value },
|
||||||
|
success: function (res) {
|
||||||
|
Swal.fire({ icon: 'success', title: 'Berhasil!', text: res.message, showConfirmButton: false, timer: 1500 })
|
||||||
|
.then(() => location.reload());
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const res = xhr.responseJSON || {};
|
||||||
|
const errors = res.errors ? Object.values(res.errors).flat().join('<br>') : null;
|
||||||
|
Swal.fire({ icon: 'error', title: 'Gagal!', html: errors || res.message || 'Gagal menyimpan komoditas.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<script>
|
<script>
|
||||||
document.write(new Date().getFullYear());
|
document.write(new Date().getFullYear());
|
||||||
</script>
|
</script>
|
||||||
, by <a href="#" target="_blank" class="footer-link">Yayasan Bhumi Loka Lestari</a>
|
, by <a href="#" target="_blank" class="footer-link">PT Republik Solusi</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user