Files
database-pertani-web/AISTANDAR.md
T

31 KiB
Raw Blame History

AISTANDAR — Standar Pengembangan Aplikasi database-pertani-web

Dokumen ini adalah aturan wajib bagi AI (dan developer) yang bekerja di repo ini. Baca seluruhnya sebelum membuat, mengubah, atau merefaktor kode.

0. Cara memakai dokumen ini

  • Aturan di sini mengalahkan kebiasaan umum atau contoh dari luar. Jika ada konflik antara kode lama dan dokumen ini, ikuti dokumen ini untuk kode baru, dan ikuti Bagian 13 (Prosedur Refaktor) untuk kode lama.
  • Jika sesuatu tidak diatur di sini: lihat dulu bagaimana modul yang sudah sesuai standar melakukannya, lalu tiru polanya. Jika tetap tidak jelas, tanya, jangan mengarang pola baru.
  • Jangan menambah package/library baru, mengubah struktur database yang sudah ada, atau mengubah kontrak API tanpa izin eksplisit.
  • Bahasa: teks yang tampil ke pengguna (label, pesan, validasi) memakai Bahasa Indonesia. Nama domain (petani, lahan, komoditas) tetap Bahasa Indonesia; kata kerja teknis Laravel (index, store, show) tetap bahasa Inggris.

1. Stack

Bagian Teknologi
Framework Laravel 12
Frontend web Blade (tanpa Livewire/Inertia)
UI template Vuexy (Bootstrap 5), ikon Tabler
Grafik ApexCharts
Peta Leaflet
Auth web Session
Auth API Laravel Sanctum (token)
Dokumentasi API dedoc/scramble
Role app/Enums/Role.php: super_admin, admin (admin program), koordinator (per komoditas), enumerator (penginput data petani), fasilitator (pemateri workshop), petani, offtaker

2. Struktur folder & penamaan

2.1 Pengelompokan per domain

Controller, Model, Request, dan View dikelompokkan per domain yang sama:

Domain yang ada: Dashboard, Laporan, Produktivitas, Ekosistem, Program, Master,
                 Bumdes, Koperasi, Konfigurasi
Lapisan Lokasi
Controller web app/Http/Controllers/Web/{Domain}/{Model}Controller.php
Controller API app/Http/Controllers/Api/{Domain}/{Model}Controller.php
Form Request app/Http/Requests/{Domain}/Store{Model}Request.php, Update{Model}Request.php
Model app/Models/{DomainModel}/{Model}.php (ikuti folder model yang ada: Pertanian/, Wilayah/, Program/, Master/, dst.)
Enum app/Enums/
Service / Query app/Services/{Domain}/
View resources/views/{domain}/{modul}/ (huruf kecil, kebab-case)

Controller baru tidak boleh ditaruh di root app/Http/Controllers/. Controller root yang sudah ada (ProgramController, ProgramKomoditasController, ProgramLokasiController) dipindahkan ke domain Program/ saat direfaktor.

2.2 Konvensi nama

Objek Aturan Contoh
Model Singular, PascalCase Petani, JenisKomoditas
Tabel snake_case, selalu ditulis eksplisit di model protected $table = 'petani';
Controller {Model}Controller PetaniController
Route name {modul}.{aksi}, kebab-case jenis-komoditas.index
URL kebab-case /jenis-komoditas
View {domain}/{modul}/{aksi}.blade.php master/provinsi/index.blade.php
Method tambahan camelCase byProvinsi(), list()

Karena nama domain berbahasa Indonesia, pluralisasi otomatis Laravel salah (petanis). Wajib set $table di setiap model, dan set parameter route bila perlu (->parameters(['jenis-komoditas' => 'jenisKomoditas'])).


3. Controller

Cakupan: Bagian 3 berlaku untuk modul CRUD (pengelolaan data: tambah, ubah, hapus). Controller Dashboard dan Laporan/Reporting tidak terikat aturan 7 method dan mengikuti aturan yang lebih fleksibel di Bagian 11.

3.1 Tujuh method resmi

Untuk modul CRUD, gunakan hanya nama method resource Laravel:

Method HTTP URL Route name Fungsi
index GET /items items.index Daftar data
create GET /items/create items.create Form tambah
store POST /items items.store Simpan baru
show GET /items/{item} items.show Detail
edit GET /items/{item}/edit items.edit Form ubah
update PUT/PATCH /items/{item} items.update Simpan perubahan
destroy DELETE /items/{item} items.destroy Hapus (soft delete)

Dilarang memakai nama add, view, save, delete, remove, detail, atau sejenisnya untuk aksi CRUD. (Larangan ini tidak berlaku untuk Dashboard dan Laporan, lihat Bagian 11.)

3.2 Prinsip controller tipis

  • Validasi selalu di Form Request, tidak pernah $request->validate() di controller.
  • Controller hanya: terima request → panggil model/service → kembalikan respons.
  • Logika bisnis yang lebih dari ±15 baris, dipakai di lebih dari satu tempat, atau melibatkan banyak tabel → pindahkan ke app/Services/{Domain}/.
  • Operasi yang menulis ke lebih dari satu tabel → bungkus dengan DB::transaction().
  • Pakai route model binding (Petani $petani), bukan find($id).

3.3 Tiga pola resmi

Pilih satu pola per modul berdasarkan tabel ini:

Pola Kapan dipakai Contoh
A. Offcanvas (AJAX) Data referensi/sederhana: sedikit field, tanpa halaman detail yang kaya Provinsi, Kabupaten, Komoditas, Varietas, Adat
B. Resource halaman Entitas utama: banyak field, ada relasi, perlu halaman detail Lahan, Produksi, Workshop, BUMDes, Koperasi
C. Detail multi-tab Entitas induk dengan banyak bagian data yang diisi bertahap Petani, Survey Kelompok Tani

Pola A — Offcanvas (AJAX)

  • index mengembalikan view; store, show, update, destroy mengembalikan JSON.
  • Tidak ada halaman create/edit; form ada di offcanvas di index.blade.php.
  • show dipakai JS untuk mengisi form edit di offcanvas.
// 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.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/
└── ...
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).
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)

  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:
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();
            }
        });
    }
}
  1. 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, 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.
  1. 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.

// ❌ SALAH — route CRUD manual
Route::get('provinsi', [ProvinsiController::class, 'index'])->name('provinsi.index');
Route::post('provinsi/store', [ProvinsiController::class, 'store'])->name('provinsi.store');
Route::get('provinsi/view/{id}', [ProvinsiController::class, 'view'])->name('provinsi.view');
Route::post('provinsi/update/{id}', [ProvinsiController::class, 'update'])->name('provinsi.update');
Route::get('provinsi/delete/{id}', [ProvinsiController::class, 'destroy'])->name('provinsi.destroy');

// ✅ BENAR
Route::resource('provinsi', ProvinsiController::class)->except(['create', 'edit']);

Aturan turunannya:

Kebutuhan Cara
Tidak semua aksi dipakai ->only([...]) atau ->except([...])
Pola A (offcanvas) ->except(['create', 'edit'])
Pola B (resource halaman) Route::resource(...) penuh
Pola C (multi-tab) Induk Route::resource, tab Route::resource('induk.tab', ...)->shallow()
Beberapa modul sekaligus Route::resources([...]) boleh dipakai dalam satu grup domain
Nama parameter tidak sesuai model ->parameters(['jenis-komoditas' => 'jenisKomoditas'])
Method tambahan (list, by{Induk}, verify, export) Route::get/post/patch biasa, ditulis sebelum Route::resource modul yang sama
  • Form hapus/ubah memakai method spoofing Blade (@method('DELETE'), @method('PUT')), dan AJAX mengirim method HTTP yang sesuai. Jangan memakai GET untuk menghapus.
  • Pengecualian: Dashboard dan Laporan tidak memakai Route::resource (lihat Bagian 11). Halaman tunggal seperti Profil akun boleh memakai Route::singleton('profil', ...) atau route manual edit/update.

6.2 Aturan routing lainnya

  • Semua route web di routes/web.php, API di routes/api.php.
  • Kelompokkan per domain dengan Route::middleware(...)->group() dan komentar judul domain.
  • Nested resource memakai ->shallow(). Maksimal dua tingkat di URL (/framework/{framework}/transformasi). Hierarki lebih dalam (Framework → Transformasi → Aktivitas → Target) dipecah menjadi beberapa nested shallow resource berpasangan.
  • Satu route hanya didaftarkan satu kali. Sebelum menambah route, cari dulu apakah sudah ada (php artisan route:list --name=...).
  • Setelah login: enumerator → Dashboard Survei Petani; fasilitator → Profil akun (belum ada menu khusus); role lain → Dashboard Karakteristik Petani. Jangan mengubah logika ini tanpa izin.

7. Blade & Vuexy

7.1 Layout

  • Semua halaman admin: @extends('layout.main') dan mengisi @section('content').
  • Halaman auth memakai auth/layout.blade.php.
  • Jangan membuat layout baru; jangan menyalin <head>/sidebar ke halaman.

7.2 File view per pola

File Pola Isi
index.blade.php A, B, C Tabel, pencarian, pagination (+ offcanvas form untuk Pola A)
create.blade.php B, C Form tambah, @include('..._form')
edit.blade.php B, C Form ubah, @include('..._form')
show.blade.php B, C Detail
_form.blade.php A, B, C Field form bersama untuk tambah dan ubah
header.blade.php C Kartu profil + navigasi tab
  • File add.blade.php dan view.blade.php tidak boleh dibuat lagi (ganti dengan create dan show).
  • _form.blade.php menerima model (kosong saat tambah) dan memakai old('field', $model->field).

7.3 Tampilan error validasi

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

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 Role yang sudah ada. Ikuti mekanisme pembatasan role yang sudah dipakai (middleware/policy/gate yang ada); jangan membuat sistem permission baru.
  • Penginput data petani adalah role enumerator (bukan fasilitator). fasilitator khusus pemateri workshop.
  • Semua route petani/... dibungkus middleware akses.petani (BatasiAksesPetani): hanya super_admin, admin, koordinator, enumerator; koordinator hanya boleh mengakses petani dengan komoditas utama yang ditugaskan (koordinator_komoditas, cek lewat User::bolehAksesPetani()). Route tab petani baru wajib diletakkan di dalam grup ini.
  • Menu sidebar hanya menampilkan item yang boleh diakses role pengguna, dan route-nya juga dilindungi di sisi server. Menyembunyikan menu saja tidak cukup.

10. API (app/Http/Controllers/Api/)

  • API dipakai aplikasi mobile/lapangan. Kontrak API (URL, nama field, format respons) tidak boleh diubah tanpa izin eksplisit, termasuk saat refaktor.
  • Endpoint baru mengikuti pola dan format respons API yang sudah ada di folder Api/.
  • Auth memakai Sanctum; login dibatasi rate limit (5/menit). Jangan longgarkan.
  • Setiap endpoint baru harus terbaca rapi di dokumentasi Scramble (type-hint Form Request dan tipe return yang jelas).

11. Pengecualian

11.0 Prinsip umum Dashboard & Laporan

Dashboard dan laporan bersifat baca (read-only) dan bentuknya beragam, sehingga controller-nya fleksibel:

  • Nama method bebas, asalkan deskriptif dan camelCase (index, chartProduktivitas, petaSebaran, rekapPerDesa, exportExcel).
  • Boleh satu controller dengan banyak method, atau controller satu aksi (__invoke) jika halamannya sederhana.
  • Route ditulis manual (Route::get(...)), tidak memakai Route::resource.
  • Tidak ada create/store/edit/update/destroy. Jika sebuah dashboard butuh input data, input itu dibuat sebagai modul CRUD terpisah.

Yang tetap berlaku walau fleksibel:

  • Lokasi folder per domain (Bagian 2), penamaan route kebab-case dengan prefix (dashboard.*, laporan.*).
  • Validasi filter memakai Form Request jika filternya lebih dari 2–3 parameter.
  • Query sederhana boleh di controller. Query agregat yang panjang, berat, atau dipakai di lebih dari satu tempat dipindah ke app/Services/{Dashboard|Laporan}/.
  • Data yang ditampilkan tetap menghormati soft delete (data terhapus tidak ikut dihitung) dan global scope program.
  • Pembatasan akses per role (Bagian 9).

11.1 Dashboard

  • Controller: app/Http/Controllers/Web/Dashboard/{Nama}Controller.php.
  • Pola yang disarankan: index mengembalikan halaman; data grafik/peta yang dimuat via AJAX memakai method bernama jelas yang mengembalikan JSON.
  • Filter umum diterima lewat query string: program, wilayah (provinsi → desa), periode.
  • Query berat di-cache (Cache::remember) dengan kunci yang memuat nilai filter.
  • View: satu file per dashboard di resources/views/dashboard/. Grafik ApexCharts, peta Leaflet.
  • Data eksternal (AlphaVantage di InternationalMarketController) wajib di-cache dan punya penanganan gagal (tampilkan pesan, jangan error 500).
// 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.
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