diff --git a/AISTANDAR.md b/AISTANDAR.md new file mode 100644 index 0000000..444fe81 --- /dev/null +++ b/AISTANDAR.md @@ -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 `
`/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 + +@error('nama') +