31 KiB
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$tabledi 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), bukanfind($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)
indexmengembalikan view;store,show,update,destroymengembalikan JSON.- Tidak ada halaman
create/edit; form ada di offcanvas diindex.blade.php. showdipakai JS untuk mengisi form edit di offcanvas.
// 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']);
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}].
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.
Route::resource('lahan', LahanController::class);
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.phpberisi 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/
└── ...
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 sebelumRoute::resource.
4. Validasi
- Setiap
storedanupdatememakai Form Request:Store{Model}Request,Update{Model}Request. - Pesan dan atribut dalam Bahasa Indonesia (
messages()/attributes(), atau filelang/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).
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
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:
$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)
- Semua penghapusan adalah soft delete.
forceDelete()dilarang di kode aplikasi. deleted_bywajib 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:
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();
}
});
}
}
- Hapus berantai (cascade soft delete). Jika induk dihapus, semua data anaknya ikut di-soft delete, termasuk cucu dan seterusnya. Tulis di model induk:
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, sehinggadeleted_bykosong dan cascade terputus. - Setiap kali menambah relasi anak baru, tambahkan juga ke blok
deletinginduknya. - Pemanggilan
delete()di controller selalu dibungkusDB::transaction(). - Relasi
belongsToke data master (Provinsi, Komoditas, dll.) bukan anak dan tidak ikut terhapus.
- Restore belum disediakan di aplikasi; dilakukan langsung di database oleh admin. Jangan membuat halaman sampah/restore tanpa diminta.
5.4 Global scope
KomoditasProgramScopeotomatis 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.
// ❌ 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 memakaiGETuntuk menghapus. - Pengecualian: Dashboard dan Laporan tidak memakai
Route::resource(lihat Bagian 11). Halaman tunggal seperti Profil akun boleh memakaiRoute::singleton('profil', ...)atau route manualedit/update.
6.2 Aturan routing lainnya
- Semua route web di
routes/web.php, API diroutes/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.phpdanview.blade.phptidak boleh dibuat lagi (ganti dengancreatedanshow). _form.blade.phpmenerima model (kosong saat tambah) dan memakaiold('field', $model->field).
7.3 Tampilan error validasi
<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-TOKENdanAccept: application/json. - URL di JS diambil dari
route()yang dicetak Blade (mis. atributdata-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)
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)
{ "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
Roleyang 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(bukanfasilitator).fasilitatorkhusus pemateri workshop. - Semua route
petani/...dibungkus middlewareakses.petani(BatasiAksesPetani): hanyasuper_admin,admin,koordinator,enumerator; koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan (koordinator_komoditas, cek lewatUser::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 memakaiRoute::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:
indexmengembalikan 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).
// 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 diresources/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
exportdi 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.
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, filecomposerlockyang 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
- Satu modul per sesi/commit. Jangan merefaktor beberapa modul sekaligus.
- Jangan merefaktor modul yang sedang dikerjakan (ada perubahan belum di-commit) tanpa konfirmasi.
- API tidak ikut diubah (Bagian 10).
- Perilaku untuk pengguna harus tetap sama; refaktor bukan tempat menambah fitur.
13.2 Langkah
- Simpan daftar route modul:
php artisan route:list --name={modul} > /tmp/before.txt. - Tentukan pola target (A/B/C) sesuai 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. - 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_byterisi, dan cascade ke anak sudah ditulis; - semua referensi yang ditemukan di langkah 3.
- method
- Bandingkan
route:listsesudah dengan/tmp/before.txt; pastikan tidak ada route yang hilang tanpa sengaja. - Uji manual alur lengkap: index → tambah → detail → ubah → hapus (cek
deleted_at,deleted_by, dan data anak). - 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:
$tableeksplisit,$fillable,SoftDeletes,deleted_byterisi, casts/Enum - Relasi anak ditambahkan ke cascade
deletingdi model induk - Form Request
Store/Updatedengan pesan Bahasa Indonesia dan uniquewithoutTrashed() - 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,_formsesuai 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