# 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')