Update Praktik Budidaya
This commit is contained in:
+712
@@ -0,0 +1,712 @@
|
||||
# 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` (atau lokasi Role yang sudah ada): `super_admin`, `admin`, `fasilitator`, `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: `fasilitator` → Dashboard Survei Petani; 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.
|
||||
- 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
|
||||
@@ -184,6 +184,12 @@ class PraktikBudidayaController extends Controller
|
||||
|
||||
$petani = Petani::findOrFail($petaniId);
|
||||
|
||||
if (!$petani->isKomoditasVanili()) {
|
||||
$validatedData['penyerbukan_manual'] = null;
|
||||
$validatedData['alasan_tidak_penyerbukan'] = null;
|
||||
$validatedData['tingkat_keberhasilan_penyerbukan'] = null;
|
||||
}
|
||||
|
||||
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
|
||||
|
||||
$praktikBudidaya = $petani->praktikBudidaya()->create([
|
||||
@@ -567,6 +573,12 @@ class PraktikBudidayaController extends Controller
|
||||
|
||||
$pemupukanTidakPernah = (int) ($validatedData['pemberian_pupuk'] ?? 0) === PemberianPupuk::TIDAK_PERNAH->value;
|
||||
|
||||
if (!$petani->isKomoditasVanili()) {
|
||||
$validatedData['penyerbukan_manual'] = null;
|
||||
$validatedData['alasan_tidak_penyerbukan'] = null;
|
||||
$validatedData['tingkat_keberhasilan_penyerbukan'] = null;
|
||||
}
|
||||
|
||||
$dataPraktikBudidaya = [
|
||||
'tenaga_kerja' => $validatedData['tenaga_kerja'],
|
||||
'frekuensi_pemangkasan_utama' => $validatedData['frekuensi_pemangkasan_komoditas_utama'],
|
||||
@@ -682,7 +694,7 @@ class PraktikBudidayaController extends Controller
|
||||
'uid' => 'SP-' . strtoupper(Str::random(10)),
|
||||
'petani_id' => $petani->id,
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by_by' => auth()->id()
|
||||
'updated_by' => auth()->id()
|
||||
], $dataSurveyPanen));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ class SurveyProduktivitasController extends Controller
|
||||
{
|
||||
$petani = Petani::findOrFail($petaniId);
|
||||
|
||||
// Baris yang benar-benar kosong dilewati (sama seperti Pendidikan & Pelatihan)
|
||||
$request->merge([
|
||||
'produktivitas_komoditas' => collect($request->input('produktivitas_komoditas', []))
|
||||
->filter(fn ($row) => filled($row['komoditas_id'] ?? null)
|
||||
|| filled($row['hasil_panen_3_musim'] ?? null)
|
||||
|| filled($row['satuan'] ?? null))
|
||||
->values()
|
||||
->all(),
|
||||
]);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'produktivitas_komoditas' => ['required', 'array', 'min:1'],
|
||||
'produktivitas_komoditas.*.komoditas_id' => ['required', 'exists:master_komoditas,id'],
|
||||
|
||||
@@ -113,6 +113,12 @@ class Petani extends Model
|
||||
return $this->belongsTo(Komoditas::class, 'komoditas_id');
|
||||
}
|
||||
|
||||
// Pertanyaan penyerbukan manual hanya berlaku untuk petani vanili.
|
||||
public function isKomoditasVanili(): bool
|
||||
{
|
||||
return str_contains(strtolower($this->komoditas?->nama ?? ''), 'vanil');
|
||||
}
|
||||
|
||||
public function skor(): HasOne
|
||||
{
|
||||
return $this->hasOne(SkorPetani::class, 'petani_id');
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
@error('frekuensi_pemangkasan_pohon_penaung')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
@if ($petani->isKomoditasVanili())
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
<label class="form-label d-block fw-medium">Penyerbukan Manual<span class="text-danger">*</span></label>
|
||||
@@ -228,6 +229,7 @@
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<h5 class="pb-4 border-bottom mb-4">Praktik Pemupukan</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
@error('frekuensi_pemangkasan_pohon_penaung')<div class="error invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
@if ($petani->isKomoditasVanili())
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
<label class="form-label d-block fw-medium">Penyerbukan Manual<span class="text-danger">*</span></label>
|
||||
@@ -260,6 +261,7 @@
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<h5 class="pb-4 border-bottom mb-4">Praktik Pemupukan</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4 form-control-validation">
|
||||
@@ -1406,7 +1408,7 @@
|
||||
<option value="">Pilih tingkat hasil panen</option>
|
||||
@foreach (TingkatPanenReject::options() as $option)
|
||||
<option value="{{ $option['value'] }}"
|
||||
{{ old('praktik_sortasi', $surveyPanen?->praktik_sortasi) == $option['value'] ? 'selected' : '' }}>
|
||||
{{ old('tingkat_reject', $surveyPanen?->hasil_panen_reject) == $option['value'] ? 'selected' : '' }}>
|
||||
{{ $option['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
@@ -1423,7 +1425,7 @@
|
||||
class="form-control @error('hasil_panen_reject') is-invalid @enderror"
|
||||
id="hasil-panen-reject"
|
||||
name="hasil_panen_reject"
|
||||
value="{{ old('hasil_panen_reject', $surveyPanen?->hasil_panen_reject) }}"
|
||||
value="{{ old('hasil_panen_reject', $surveyPanen?->jumlah_panen_reject) }}"
|
||||
step="0.01"
|
||||
min="0">
|
||||
<span class="input-group-text">Kg/Musim</span>
|
||||
@@ -1560,6 +1562,9 @@
|
||||
Produktivitas Rata-rata 3 Musim Panen Terakhir per Komoditas
|
||||
</h5>
|
||||
{{-- DATA LAMA --}}
|
||||
@php
|
||||
$labelSatuanProduktivitas = ['ha_tahun' => 'Ha/Tahun', 'pohon_tahun' => 'Pohon/Tahun'];
|
||||
@endphp
|
||||
@if ($surveyProduktivitas->count() > 0)
|
||||
@foreach ($surveyProduktivitas as $row)
|
||||
<div class="row align-items-end border-bottom pb-3 mb-3">
|
||||
@@ -1580,11 +1585,13 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Satuan</label>
|
||||
<input type="text" class="form-control" value="{{ $row->satuan }}" disabled>
|
||||
<input type="text" class="form-control" value="{{ $labelSatuanProduktivitas[$row->satuan] ?? $row->satuan }}" disabled>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<form method="POST" action="{{ route('petani.survey-produktivitas.destroy', [$petani->id, $row->id]) }}">
|
||||
<form method="POST"
|
||||
action="{{ route('petani.survey-produktivitas.destroy', [$petani->id, $row->id]) }}"
|
||||
class="delete-db-form">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<input type="hidden" name="redirect_fragment" value="section-produktivitas">
|
||||
@@ -1595,12 +1602,14 @@
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<p class="text-muted mb-4">Belum ada data produktivitas.</p>
|
||||
@endif
|
||||
|
||||
{{-- FORM TAMBAH DATA BARU --}}
|
||||
<form method="POST"
|
||||
action="{{ route('petani.survey-produktivitas.store', $petani->id) }}"
|
||||
class="form-repeater">
|
||||
class="form-repeater-produktivitas">
|
||||
@csrf
|
||||
<input type="hidden" name="redirect_fragment" value="section-produktivitas">
|
||||
|
||||
@@ -1625,7 +1634,7 @@
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Komoditas</label>
|
||||
<select name="komoditas_id" class="form-select @error('produktivitas_komoditas.' . $index . '.komoditas_id') is-invalid @enderror">
|
||||
<select name="komoditas_id" class="form-select produktivitas-input @error('produktivitas_komoditas.' . $index . '.komoditas_id') is-invalid @enderror">
|
||||
<option value="">Pilih Komoditas</option>
|
||||
@foreach ($komoditas as $item)
|
||||
<option value="{{ $item->id }}" @selected((string) data_get($produktivitasRow, 'komoditas_id') === (string) $item->id)>{{ $item->nama }}</option>
|
||||
@@ -1639,7 +1648,7 @@
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Hasil</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="hasil_panen_3_musim" class="form-control @error('produktivitas_komoditas.' . $index . '.hasil_panen_3_musim') is-invalid @enderror" step="0.01" min="0" value="{{ data_get($produktivitasRow, 'hasil_panen_3_musim') }}">
|
||||
<input type="number" name="hasil_panen_3_musim" class="form-control produktivitas-input @error('produktivitas_komoditas.' . $index . '.hasil_panen_3_musim') is-invalid @enderror" step="0.01" min="0" value="{{ data_get($produktivitasRow, 'hasil_panen_3_musim') }}">
|
||||
<span class="input-group-text">Kg</span>
|
||||
</div>
|
||||
@error('produktivitas_komoditas.' . $index . '.hasil_panen_3_musim')
|
||||
@@ -1651,12 +1660,12 @@
|
||||
<label class="form-label d-block">Satuan</label>
|
||||
|
||||
<label class="form-check form-check-inline">
|
||||
<input class="form-check-input @error('produktivitas_komoditas.' . $index . '.satuan') is-invalid @enderror" type="radio" name="satuan" value="ha_tahun" @checked(data_get($produktivitasRow, 'satuan') === 'ha_tahun')>
|
||||
<input class="form-check-input produktivitas-satuan @error('produktivitas_komoditas.' . $index . '.satuan') is-invalid @enderror" type="radio" name="satuan" value="ha_tahun" @checked(data_get($produktivitasRow, 'satuan') === 'ha_tahun')>
|
||||
<span class="form-check-label">Ha/Tahun</span>
|
||||
</label>
|
||||
|
||||
<label class="form-check form-check-inline">
|
||||
<input class="form-check-input @error('produktivitas_komoditas.' . $index . '.satuan') is-invalid @enderror" type="radio" name="satuan" value="pohon_tahun" @checked(data_get($produktivitasRow, 'satuan') === 'pohon_tahun')>
|
||||
<input class="form-check-input produktivitas-satuan @error('produktivitas_komoditas.' . $index . '.satuan') is-invalid @enderror" type="radio" name="satuan" value="pohon_tahun" @checked(data_get($produktivitasRow, 'satuan') === 'pohon_tahun')>
|
||||
<span class="form-check-label">Pohon/Tahun</span>
|
||||
</label>
|
||||
@error('produktivitas_komoditas.' . $index . '.satuan')
|
||||
@@ -1665,7 +1674,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 mb-3">
|
||||
<button type="button" class="btn btn-outline-danger" data-repeater-delete>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-produktivitas" data-repeater-delete>
|
||||
<i class="icon-base ti tabler-trash icon-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1677,7 +1686,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-outline-success" data-repeater-create>
|
||||
<button type="button" class="btn btn-outline-success" id="btn-tambah-produktivitas" data-repeater-create>
|
||||
<i class="icon-base ti tabler-plus me-1_5 icon-sm"></i>
|
||||
Tambah Komoditas
|
||||
</button>
|
||||
@@ -1685,7 +1694,8 @@
|
||||
<div class="d-flex flex-wrap justify-content-end gap-2">
|
||||
<a href="{{ route('petani.index') }}#data-petani" class="btn btn-secondary">Kembali</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
Simpan
|
||||
<i class="icon-base ti tabler-check me-1_5 icon-sm"></i>
|
||||
Simpan Produktivitas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2113,14 +2123,112 @@
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Repeater produktivitas: perilakunya disamakan dengan halaman Pendidikan & Pelatihan.
|
||||
$(function () {
|
||||
$('.form-repeater').repeater({
|
||||
show: function () {
|
||||
$(this).slideDown();
|
||||
},
|
||||
hide: function (deleteElement) {
|
||||
$(this).slideUp(deleteElement);
|
||||
const $form = $('.form-repeater-produktivitas');
|
||||
const $btnTambah = $('#btn-tambah-produktivitas');
|
||||
|
||||
if (!$form.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
function rowAdaIsi($row) {
|
||||
const adaInput = $row.find('.produktivitas-input').toArray().some(function (input) {
|
||||
return String($(input).val() || '').trim() !== '';
|
||||
});
|
||||
|
||||
return adaInput || $row.find('.produktivitas-satuan:checked').length > 0;
|
||||
}
|
||||
|
||||
function rowLengkap($row) {
|
||||
const inputLengkap = $row.find('.produktivitas-input').toArray().every(function (input) {
|
||||
return String($(input).val() || '').trim() !== '';
|
||||
});
|
||||
|
||||
return inputLengkap && $row.find('.produktivitas-satuan:checked').length > 0;
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
const $rows = $form.find('[data-repeater-item]');
|
||||
|
||||
$rows.each(function () {
|
||||
$(this).find('.btn-delete-produktivitas').prop('disabled', !rowAdaIsi($(this)));
|
||||
});
|
||||
|
||||
$btnTambah.prop('disabled', !rowLengkap($rows.last()));
|
||||
}
|
||||
|
||||
$form.repeater({
|
||||
initEmpty: false,
|
||||
|
||||
show: function () {
|
||||
const $row = $(this);
|
||||
|
||||
$row.find('.produktivitas-input').val('');
|
||||
$row.find('.produktivitas-satuan').prop('checked', false);
|
||||
$row.find('.is-invalid').removeClass('is-invalid');
|
||||
$row.find('.invalid-feedback').remove();
|
||||
$row.slideDown();
|
||||
|
||||
setTimeout(updateButtons, 50);
|
||||
},
|
||||
|
||||
hide: function (deleteElement) {
|
||||
const $row = $(this);
|
||||
|
||||
if (!rowAdaIsi($row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hanya menghapus baris di form, tidak mengirim ke database
|
||||
Swal.fire({
|
||||
title: 'Hapus baris produktivitas ini?',
|
||||
text: 'Baris ini hanya akan dihapus dari form.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, hapus',
|
||||
cancelButtonText: 'Batal',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger me-3',
|
||||
cancelButton: 'btn btn-label-secondary'
|
||||
},
|
||||
buttonsStyling: false
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
$row.slideUp(deleteElement);
|
||||
setTimeout(updateButtons, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$form.on('input keyup change', '.produktivitas-input, .produktivitas-satuan', updateButtons);
|
||||
|
||||
updateButtons();
|
||||
|
||||
// Konfirmasi hapus data yang sudah tersimpan di database
|
||||
$(document).on('submit', '.delete-db-form', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = this;
|
||||
|
||||
Swal.fire({
|
||||
title: 'Hapus data ini?',
|
||||
text: 'Data yang sudah dihapus tidak bisa dikembalikan.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, hapus',
|
||||
cancelButtonText: 'Batal',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger me-3',
|
||||
cancelButton: 'btn btn-label-secondary'
|
||||
},
|
||||
buttonsStyling: false
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user