# Best Practices

## PHP

### Traits
- Gabungkan multiple `use` trait menjadi satu baris dengan koma.
- **Hindari:**
  ```php
  use FlashesEntityMessage;
  use ParsesDataTableQuery;
  ```
- **Gunakan:**
  ```php
  use FlashesEntityMessage, ParsesDataTableQuery;
  ```

### Model Eloquent

- **Urutan Penulisan di dalam Model Class:**
  Setiap model harus disusun mengikuti urutan struktur berikut dari atas ke bawah:
  1. **Use Trait** (misal: `use HasFactory, SoftDeletes;`).
  2. **Casting** (metode `casts()`).
  3. **Scope** (metode local scope dengan PHP attribute `#[Scope]`).
  4. **Attribute** (Accessors / Appends - misal format harga, format tanggal, dll. Harus dimasukkan dalam array `$appends` di atas class).
  5. **Method Lainnya** (helper method, logic bisnis, dll.).
  6. **Relasi (Relationships)** (diurutkan secara alfabetis berdasarkan nama method relasi).

- **Pengelompokan Scope:**
  - Scope dikelompokkan berdasarkan kolom yang di-query.
  - Urutkan berdasarkan nama kolom (abjad), lalu di dalam setiap Column Group, urutkan lagi berdasarkan nama method (abjad).
  - **Contoh:** Jika ada kolom `is_active` dan `status`:
    ```php
    // Column group: is_active (first alphabetical)
    #[Scope]
    public function active(Builder $query): void
    {
        $query->where('is_active', true);
    }

    #[Scope]
    public function inactive(Builder $query): void
    {
        $query->where('is_active', false);
    }

    // Column group: status (second alphabetical)
    #[Scope]
    public function completed(Builder $query): void
    {
        $query->where('status', OrderStatus::COMPLETED);
    }

    #[Scope]
    public function pending(Builder $query): void
    {
        $query->where('status', OrderStatus::PENDING);
    }
    ```

- **Hubungan Timbal Balik (2-Way Relations):**
  - Pastikan setiap relasi ditulis secara 2 arah (bi-directional).
  - Jika suatu model memiliki `belongsTo` ke model lain, pastikan model lain tersebut juga mendefinisikan relasi kebalikannya (`hasMany` atau `hasOne`).

- **Casting:**
  - Lakukan casting secara tepat pada kolom yang memerlukan tipe data khusus.
  - Tipe data yang perlu di-cast: `enum`, `boolean`, `integer`, `decimal`, `date`, `datetime`, `array`, `json`, dll.
  - **Jangan** cast kolom relasi (foreign key).
  - **Contoh:**
    ```php
    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class,      // enum
            'is_active' => 'boolean',             // boolean
            'amount' => 'integer',                // integer
            'price' => 'decimal:2',               // decimal
            'birth_date' => 'date',               // date
            'created_at' => 'datetime',           // datetime
            'settings' => 'array',                // array/json
        ];
    }
    ```

- **Scope untuk Kolom Opsi:**
  - Semua kolom yang memiliki opsi/status tertentu (seperti `status`, `is_active`, `type`, dll.) **wajib** memiliki scope.
  - Gunakan PHP attribute `#[Scope]` di atas metode scope.

- **Attribute untuk Format:**
  - Kolom yang akan diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal) **wajib** menggunakan accessor dengan class `Attribute`.
  - Nama atribut harus dimasukkan dalam properti `$appends` di atas class.
  - **Contoh:** `amount` → `amount_formatted`, `status` → `status_label`.

- **Contoh Lengkap:**
  ```php
  namespace App\Models;

  use App\Enums\OrderStatus;
  use Illuminate\Database\Eloquent\Attributes\Appends;
  use Illuminate\Database\Eloquent\Attributes\Guarded;
  use Illuminate\Database\Eloquent\Attributes\Scope;
  use Illuminate\Database\Eloquent\Builder;
  use Illuminate\Database\Eloquent\Casts\Attribute;
  use Illuminate\Database\Eloquent\Factories\HasFactory;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
  use Illuminate\Database\Eloquent\Relations\HasMany;
  use Illuminate\Database\Eloquent\SoftDeletes;

  #[Guarded(['id'])]
  #[Appends(['amount_formatted', 'status_label'])]
  class Order extends Model
  {
      // 1. Use Trait
      use HasFactory, SoftDeletes;

      // 2. Casting
      protected function casts(): array
      {
          return [
              'status' => OrderStatus::class,
              'amount' => 'integer',
          ];
      }

      // 3. Scope (grouped by column, then alphabetical)
      #[Scope]
      public function completed(Builder $query): void
      {
          $query->where('status', OrderStatus::COMPLETED);
      }

      #[Scope]
      public function pending(Builder $query): void
      {
          $query->where('status', OrderStatus::PENDING);
      }

      // 4. Attribute
      public function amountFormatted(): Attribute
      {
          return Attribute::make(
              get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
          );
      }

      public function statusLabel(): Attribute
      {
          return Attribute::make(
              get: fn () => $this->status->label(),
          );
      }

      // 5. Method Lainnya
      public static function mediaModuleName(): string
      {
          return 'order';
      }

      // 6. Relation
      public function customer(): BelongsTo
      {
          return $this->belongsTo(Customer::class);
      }

      public function items(): HasMany
      {
          return $this->hasMany(OrderItem::class);
      }
  }
  ```

### Controller

- **Tugas Controller:**
  - Controller hanya menangani **request dan response**.
  - Semua logika bisnis (validasi aturan bisnis, query kompleks, transformasi data, authorization bisnis) **wajib** dipindahkan ke **Service**.
  - Controller memanggil service, lalu mengembalikan `Inertia::render()`, `redirect()`, atau `response()->json()`.

- **Urutan Method:**
  1. `index`
  2. `create`
  3. `store`
  4. `show` (jika ada)
  5. `edit`
  6. `update`
  7. `destroy`
  8. Method tambahan (custom action) ditempatkan **di bawah** `destroy`.

- **Form Request:**
  - **Selalu** gunakan Form Request untuk validasi, sekecil apapun validasinya.
  - Authorization di Form Request menggunakan `$this->user()`, bukan helper global.

- **Authenticated User:**
  - **Selalu** ambil user dari `$request->user()` pada method controller.
  - **Jangan** gunakan `request()->user()`, `auth()->user()`, atau `Auth::user()`.

- **Docblock:**
  - **Jangan** gunakan PHPDoc/docblock pada controller.

- **Penamaan:**
  - Variable, method, file, dan elemen sistem lainnya **wajib** menggunakan **Bahasa Inggris**.
  - Teks yang ditampilkan ke user (flash message, label UI) menggunakan **Bahasa Indonesia**.
  - Kecuali nama modul yang memang sudah Bahasa Inggris (misal: `customer`, `cutting`, `order`).

- **Flash Message:**
  - Gunakan helper dari trait `FlashesEntityMessage` untuk pesan standar CRUD:
    - `flashCreated($entity)` → "{entity} berhasil ditambahkan."
    - `flashUpdated($entity)` → "{entity} berhasil diperbarui."
    - `flashDeleted($entity)` → "{entity} berhasil dihapus."
    - `flashStatusUpdated($entity)` → "Status {entity} berhasil diperbarui."
  - Gunakan `flashSuccess($message)` **hanya** untuk pesan custom (verifikasi owner, approve/reject, transisi status, dll.).

- **Struktur Folder:**
  - Jika satu modul memiliki **lebih dari satu controller**, kelompokkan dalam subfolder modul.
  - **Contoh:**
    ```
    app/Http/Controllers/Admin/Manage/
        Cutting/
            CuttingController.php
            CuttingDraftItemController.php
        Order/
            OrderController.php
            OrderDraftItemController.php
        Purchase/
            PurchaseController.php
            PurchaseDraftItemController.php
        Stock/
            StockController.php
            RetailStockController.php
        StokOpnameController.php          ← modul dengan 1 controller tetap di level parent
        OwnerVerificationController.php
    ```

- **Contoh:**
  ```php
  namespace App\Http\Controllers\Admin\Manage\Order;

  use App\Http\Controllers\Concerns\FlashesEntityMessage;
  use App\Http\Controllers\Concerns\ParsesDataTableQuery;
  use App\Http\Controllers\Controller;
  use App\Http\Requests\Admin\Manage\OrderRequest;
  use App\Models\Order;
  use App\Services\Manage\OrderService;
  use Illuminate\Http\RedirectResponse;
  use Illuminate\Http\Request;
  use Inertia\Inertia;
  use Inertia\Response;

  class OrderController extends Controller
  {
      use FlashesEntityMessage, ParsesDataTableQuery;

      public function __construct(
          private readonly OrderService $orderService,
      ) {}

      public function index(Request $request): Response
      {
          $tableQuery = $this->parseDataTableQuery($request);

          return Inertia::render('admin/manage/orders/Index', [
              'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
              'filters' => $this->dataTableFilters($tableQuery),
          ]);
      }

      public function store(OrderRequest $request): RedirectResponse
      {
          $this->orderService->create($request->validated(), $request->user());

          $this->flashCreated('Pesanan');

          return redirect()->route('admin.manage.orders.index');
      }

      public function destroy(Order $order): RedirectResponse
      {
          $this->orderService->delete($order);

          $this->flashDeleted('Pesanan');

          return redirect()->route('admin.manage.orders.index');
      }
  }
  ```

### Service

- **Tugas Service:**
  - Service menampung **semua logika bisnis** (query kompleks, transformasi data, transaksi, authorization bisnis).
  - Controller hanya memanggil service dan mengembalikan response.

- **Urutan Method:**
  1. `paginateForIndex` / method index/list
  2. `findForEdit` / `findForShow` (jika ada)
  3. `create` / `store`
  4. `update`
  5. `delete` / `destroy`
  6. Method tambahan (custom action)
  7. Method `private` di paling bawah

- **Docblock & Comment:**
  - **Jangan** gunakan PHPDoc/docblock pada service.
  - **Jangan** gunakan inline comment tipe `@var Model $variable`.

- **Pagination:**
  - Default pagination untuk data table: **25** (`->paginate(25)`).

- **Update Model:**
  - Gunakan `$model->update([...])`, **bukan** assign property lalu `$model->save()`.
  - **Hindari:**
    ```php
    $account->balance = $newBalance;
    $account->save();
    ```
  - **Gunakan:**
    ```php
    $account->update(['balance' => $newBalance]);
    ```

- **Logic di Model/Enum, Bukan Wrapper di Service:**
  - Method yang hanya meneruskan ke model/enum **wajib** ditempatkan di model/enum, bukan di service.
  - **Contoh:** `isEditable()`, `transitionStatusMessage()`, `ensureEditable()` → enum/model.
  - Service memanggil langsung: `$order->status->isEditable()`, `$status->transitionStatusMessage()`, `$order->ensureEditable()`.

- **Penamaan:**
  - Variable, method, file **Bahasa Inggris**.
  - Pesan error/flash ke user **Bahasa Indonesia**.

## Database & Migrasi

### Struktur File Migrasi
- Gunakan *anonymous class* yang meng-extend `Migration`.
- Tulis *return type hint* `: void` secara eksplisit pada method `up()` dan `down()`.
- **Urutan & Pengelompokan Kolom (Ordering & Grouping):**
  1. **Primary Key**: `$table->id()` ditaruh paling atas.
  2. **Relasi (Foreign Keys / Morphs)**: Ditaruh tepat setelah Primary Key di bagian teratas.
  3. **Kolom Data Utama**: Dikelompokkan secara logis menggunakan spasi (baris kosong) sebagai pemisah antar kelompok data.
  4. **Timestamps & Soft Deletes**: Ditaruh paling bawah.
- **Gunakan:**
  ```php
  use Illuminate\Database\Migrations\Migration;
  use Illuminate\Database\Schema\Blueprint;
  use Illuminate\Support\Facades\Schema;

  return new class extends Migration
  {
      public function up(): void
      {
          Schema::create('table_name', function (Blueprint $table) {
              $table->id();

              $table->foreignId('user_id')->constrained()->cascadeOnDelete();
              $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();

              $table->string('name', 100);
              $table->string('description', 255)->nullable();

              $table->timestamp('created_at')->useCurrent();
              $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
              $table->softDeletes();
          });
      }

      public function down(): void
      {
          Schema::dropIfExists('table_name');
      }
  };
  ```

### Definisi Kolom & Tipe Data
- **ID & Timestamps:**
  - Gunakan `$table->id()` sebagai primary key.
  - Gunakan `timestamp` eksplisit dengan default `useCurrent()` dan `useCurrentOnUpdate()`. **Hindari penggunaan `$table->timestamps()` bawaan Laravel agar format kolom presisi dan konsisten.**
  - Tambahkan `$table->softDeletes()` (atau `deleted_at`) jika model terkait menggunakan trait `SoftDeletes`.
    ```php
    $table->timestamp('created_at')->useCurrent();
    $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
    $table->softDeletes();
    ```
- **Kolom Enum:**
  - Hubungkan dengan PHP Backed Enum menggunakan `EnumName::values()`.
  - **Contoh:**
    ```php
    $table->enum('status', StatusEnum::values())->default(StatusEnum::PENDING->value);
    ```

### Relasi & Foreign Key
- Gunakan `$table->foreignId()`.
- Tentukan constraint relasi secara eksplisit dengan `constrained()` (dan sebutkan nama tabel target jika nama kolom tidak sesuai dengan nama tabel, misal: `constrained('users')`).
- Tentukan aksi penghapusan secara eksplisit (`cascadeOnDelete()`, `nullOnDelete()`, atau `restrictOnDelete()`) sesuai kebutuhan integritas data.
- **Contoh:**
  ```php
  $table->foreignId('user_id')->constrained()->cascadeOnDelete();
  $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
  ```

### Modifikasi Tabel (Alter Schema)
- Saat menambahkan kolom baru, gunakan modifier `->after('column_name')` agar tata letak kolom di database terstruktur dengan logis.
- Jika menambahkan kolom baru yang non-nullable pada tabel yang sudah berisi data, jalankan data migration di dalam method `up()` setelah schema builder:
  ```php
  DB::table('table_name')->update(['column_name' => 'default_value']);
  ```
- Di dalam method `down()`, pastikan untuk melepas constraint foreign key terlebih dahulu sebelum menghapus kolom bersangkutan. Gunakan `dropConstrainedForeignId` atau `dropForeign` untuk cara yang aman dan ringkas.
  - **Gunakan:**
    ```php
    public function down(): void
    {
        Schema::table('table_name', function (Blueprint $table) {
            $table->dropConstrainedForeignId('foreign_key_id');
            $table->dropColumn(['column_one', 'column_two']);
        });
    }
    ```

## JavaScript / Vue

### Komponen Reusable vs Page-Specific

- `resources/js/components/` → komponen **reusable** lintas page (UI primitives, catalog POS, printer, display formatter, dll.).
- `resources/js/pages/{module}/form/` dan `table/` → komponen **khusus page** (modal form, columns, actions).
- Jika komponen dipakai **lebih dari satu modul/page**, pindahkan ke `resources/js/components/{domain}/`.
  - Contoh: `PosCatalogCard`, `PosCatalogVariantThumb` → `components/catalog/`
  - Contoh: `OrderPrintButton`, `ThermalPrinterConnectButton` → `components/order/`

### Format Data dan Attribute Model

- Untuk **tampilan read-only**, gunakan field `*_formatted` dari API/model accessor.
- **Jangan** format manual di view jika accessor model sudah tersedia (misal: `stock_formatted`, `price_formatted`).
- Format **live/calc** di form (cart total, input rupiah) boleh pakai `formatRupiah()` / `RupiahInput`.
- Komponen display: `RupiahText` (`amount` + optional `formatted` prop).

### Types

- Type domain di `resources/js/types/{domain}.ts`.
- Type shared di `resources/js/types/common.ts`: `Paginated<T>`, `SelectOption`, `EnumOption`.
- Hindari duplikasi type di page; import dari `@/types/*`.

### Form Request dan View Label

- Label validasi harus **sama persis** dengan `<FieldLabel>` di view.
- Gunakan `attributes()` pada Form Request (locale `id` → `:Attribute wajib diisi.`).
- Contoh: view `Nama Produk` → `'name' => 'Nama Produk'`.
- Pesan custom untuk rule nested/business logic gunakan `messages()`.

### Pemisahan File

- Pecah file besar berdasarkan **fungsi**: catalog panel, cart panel, metadata form, composable logic.
- Target ideal: **< 300 baris** per komponen.

### Struktur Folder Page-Specific Components
Komponen yang hanya digunakan oleh satu page tertentu (misalnya modal form, kolom tabel, action button) **harus ditempatkan di folder page-nya langsung** di dalam subfolder sesuai fungsinya, bukan di `resources/js/components/`.

Folder `resources/js/components/` hanya untuk komponen yang **reusable** lintas page (seperti UI primitives, DataTable, ConfirmDialog, dll).

Subfolder di dalam page folder dikelompokkan berdasarkan fungsi:
- `form/` — modal form, form fields
- `table/` — columns definition, data-table-actions, cell renderers

- **Hindari:**
  ```
  resources/js/components/admin/master/categories/
      CategoryFormModal.vue
      columns.ts
      data-table-actions.vue
  resources/js/pages/admin/master/categories/
      Index.vue
  ```
- **Gunakan:**
  ```
  resources/js/pages/admin/master/categories/
      Index.vue
      form/
          CategoryFormModal.vue
      table/
          columns.ts
          data-table-actions.vue
  ```

### Import Path
Gunakan relative import (`./`) untuk file yang berada di folder/subfolder yang sama, bukan absolute path `@/...`.

- **Hindari:**
  ```ts
  import CategoryFormModal from '@/components/admin/master/categories/CategoryFormModal.vue';
  ```
- **Gunakan:**
  ```ts
  // Dari Index.vue ke subfolder
  import CategoryFormModal from './form/CategoryFormModal.vue';
  import { createColumns } from './table/columns';

  // Dari columns.ts ke file satu folder
  import DataTableActions from './data-table-actions.vue';
  ```

### Pola CRUD (Create/Edit)
Gunakan **modal** untuk CRUD yang sederhana (sedikit field, tidak ada nested data complex), dan **page terpisah** untuk CRUD yang kompleks.

- **Modal** — form sederhana, biasanya 1-3 field, tidak memerlukan layout khusus:
  ```
  pages/admin/master/categories/
      Index.vue
      form/
          CategoryFormModal.vue    ← modal form
      table/
          columns.ts
          data-table-actions.vue
  ```
  Contoh: categories (1 field), suppliers (3 field), customers (3 field)

- **Page terpisah** — form kompleks, banyak field, nested data, file upload, atau memerlukan layout khusus:
  ```
  pages/admin/master/raw-materials/
      Index.vue
      Create.vue                   ← halaman tambah
      Edit.vue                     ← halaman ubah
      form/
          RawMaterialForm.vue      ← form component
      table/
          RawMaterialGroupedTable.vue
          data-table-actions.vue
          raw-material-status-toggle.vue
  ```
  Contoh: raw-materials (nested prices, multi-image upload, dynamic variant rows)
