diff --git a/.ai/CONTEXT.md b/.ai/CONTEXT.md
new file mode 100644
index 0000000..b4c1869
--- /dev/null
+++ b/.ai/CONTEXT.md
@@ -0,0 +1,88 @@
+# DST Collection — Project Context
+
+## Tentang Project
+**DST Collection** adalah aplikasi ERP untuk brand pakaian wanita (daster, dress, dll). Aplikasi ini mengelola dua area bisnis utama:
+
+1. **Toko** — penjualan, produk, keuangan, HR
+2. **Konveksi** — bahan baku, cutting, pembelian bahan
+
+## Tech Stack
+| Layer | Technology |
+|-------|------------|
+| Backend | Laravel 13, PHP 8.3 |
+| Frontend | React 19, Inertia.js 3, TypeScript |
+| UI | shadcn/ui, Radix UI, Tailwind CSS 4 |
+| Auth | Laravel Fortify, Spatie Permission |
+| DB | MySQL (SoftDeletes di semua model utama) |
+
+## Roles & Akses
+
+### role: `developer`, `owner`
+- **Akses**: Semua module, semua aksi
+- **Catatan**: Selalu muncul di semua sidebar
+
+### role: `admin-toko`
+- **Akses**: Full CRUD semua module toko
+- **Modules**: Kategori, Produk, Customer, Transaksi, Restock, Kas Toko, Pengeluaran, Kasbon, Gaji, Pegawai, Presensi, Cuti, Settings
+- **Catatan**: TIDAK akses Supplier, Bahan Baku, Belanja, Cutting
+
+### role: `direktur`
+- **Akses**: Readonly semua data toko + bisa ajukan kasbon
+- **Modules**: Semua module toko (readonly), kecuali Supplier, Bahan Baku, Belanja, Cutting
+- **Catatan**: Bisa lihat semua data pegawai toko, tapi bukan admin bahan baku
+
+### role: `admin-bahan-baku`
+- **Akses**: Full CRUD untuk konveksi
+- **Modules**: Bahan Baku, Supplier, Belanja, Cutting, Pegawai (hanya role admin-bahan-baku), Kasbon (lihat semua + ACC/bayar)
+- **Catatan**: TIDAK akses module toko lainnya
+
+### role: `cashier`
+- **Akses**: Transaksi (full CRUD), Customer (full CRUD), Produk (readonly), Kasbon (CRUD own data)
+- **Modules**: Transaksi, Customer, Produk (view), Kasbon, Gaji (view), Presensi, Cuti
+
+### role: `marketing-offline`, `marketing-online`
+- **Akses**: Transaksi (readonly yang punya dia), Produk (readonly), Customer (full CRUD)
+- **Modules**: Transaksi (view only own), Produk (view), Customer, Kasbon (CRUD own data), Presensi, Cuti
+
+### role: `stok-opname`
+- **Akses**: Stok Opname (full CRUD + submit)
+- **Modules**: Stok Opname, Produk (view), Presensi, Cuti
+
+### role: `non-operator`
+- **Akses**: Basic (dashboard, presensi, cuti, kasbon)
+- **Modules**: Presensi, Cuti, Kasbon (CRUD own data)
+
+## Module Overview
+
+| # | Module | Route Group | Controller Path | Service Path |
+|---|--------|-------------|-----------------|--------------|
+| 1 | Kategori | `admin/master/categories` | `Admin/Master/CategoryController` | `Admin/Master/CategoryService` |
+| 2 | Produk | `admin/master/products` | `Admin/Master/Product/ProductController` | `Admin/Master/Product/ProductService` |
+| 3 | Bahan Baku | `admin/master/raw-materials` | `Admin/Master/RawMaterial/RawMaterialController` | `Admin/Master/RawMaterial/RawMaterialService` |
+| 4 | Supplier | `admin/master/suppliers` | `Admin/Master/SupplierController` | `Admin/Master/SupplierService` |
+| 5 | Customer | `admin/master/customers` | `Admin/Master/CustomerController` | `Admin/Master/CustomerService` |
+| 6 | Belanja | `admin/manage/purchases` | `Admin/Manage/PurchaseController` | `Admin/Manage/PurchaseService` |
+| 7 | Cutting | `admin/manage/cuttings` | `Admin/Manage/CuttingController` | `Admin/Manage/CuttingService` |
+| 8 | Transaksi | `admin/manage/transactions` | `Admin/Manage/TransactionController` | `Admin/Manage/TransactionService` |
+| 9 | Restock | `admin/manage/restocks` | `Admin/Manage/RestockController` | `Admin/Manage/RestockService` |
+| 10 | Stok Opname | `admin/manage/stok-opnames` | (via StockMutationController) | — |
+| 11 | Kas Toko | `admin/finance/cash-accounts` | `Admin/Finance/CashAccountController` | `Admin/Finance/CashAccountService` |
+| 12 | Pengeluaran | `admin/finance/expenses` | `Admin/Finance/ExpenseController` | `Admin/Finance/ExpenseService` |
+| 13 | Kasbon | `admin/finance/employee-advances` | `Admin/Finance/EmployeeAdvanceController` | `Admin/Finance/EmployeeAdvanceService` |
+| 14 | Gaji | `admin/finance/payroll-periods` | `Admin/Finance/PayrollController` | `Admin/Finance/PayrollPeriodService` |
+| 15 | Pegawai | `admin/hr/employees` | `Admin/HR/EmployeeController` | `Admin/HR/EmployeeService` |
+| 16 | Presensi | `admin/hr/attendances` | `Admin/HR/AttendanceController` | `Admin/HR/AttendanceService` |
+| 17 | Cuti | `admin/hr/leave-requests` | `Admin/HR/LeaveRequestController` | `Admin/HR/LeaveRequestService` |
+| 18 | Role & Permission | `admin/settings/roles` | `Admin/RoleController` | `Admin/Settings/RoleService` |
+| 19 | Pengaturan | `admin/settings` | `Admin/AdminSettingsController` | `Admin/AdminSettingsService` |
+
+## Statistics
+| Metric | Count |
+|--------|-------|
+| Tables | 49 |
+| Models | 42 |
+| Enums | 20 + 1 trait |
+| Services | 28 (including 3 concerns) |
+| Controllers | 30 |
+| Roles | 9 |
+| Permissions | ~120 |
diff --git a/.ai/CONVENTIONS.md b/.ai/CONVENTIONS.md
new file mode 100644
index 0000000..d733363
--- /dev/null
+++ b/.ai/CONVENTIONS.md
@@ -0,0 +1,1406 @@
+# Code Conventions
+
+> Aturan-aturan yang WAJIB diikuti saat menulis kode.
+
+---
+
+## Language Convention
+
+### Backend → English
+Variabel, function, kolom database, class, method — **SEMUA pakai English**:
+
+```php
+// ✅ Variable
+$orderId, $totalAmount, $customerName
+
+// ✅ Function/Method
+public function getOrders(): Collection
+public function calculateTotal(): int
+
+// ✅ Database columns
+$table->string('order_number');
+$table->decimal('total_amount');
+
+// ✅ Class names
+class OrderService { ... }
+class OrderRequest extends FormRequest { ... }
+```
+
+### Frontend (UI) → Indonesian
+Teks yang ditampilkan ke user — **pakai Bahasa Indonesia**:
+
+```tsx
+// ✅ Labels
+
+
+
+// ✅ Placeholders
+
+
+// ✅ Buttons
+
+
+
+
+// ✅ Flash messages
+'Produk berhasil ditambahkan'
+'Data berhasil dihapus'
+
+// ✅ Status labels (di Badge)
+
+
+```
+
+### Contoh Perbandingan
+| Elemen | English (Backend) | Indonesian (UI) |
+|--------|-------------------|-----------------|
+| Variable | `$customerName` | — |
+| DB column | `customer_name` | — |
+| Method | `getCustomerName()` | — |
+| Label | — | `Nama Customer` |
+| Placeholder | — | `Masukkan nama` |
+| Button | — | `Simpan`, `Hapus` |
+| Flash | — | `Berhasil disimpan` |
+| Status | `OrderStatus::PENDING` | `Menunggu` |
+
+---
+
+## Model
+
+### Mass Assignment
+```php
+// ✅ SELALU pakai #[Guarded(['id'])]
+#[Guarded(['id'])]
+class Product extends Model { ... }
+
+// ❌ JANGAN pakai $fillable
+protected $fillable = ['name', 'status']; // JANGAN
+```
+
+### Urutan Isi Model
+```php
+use Illuminate\Database\Eloquent\Casts\Attribute;
+
+// Attributes di ATAS class declaration
+#[Guarded(['id'])]
+#[Appends(['formatted_name'])]
+class Product extends Model
+{
+ use HasFactory, SoftDeletes;
+
+ // 1. Casts (method, wajib untuk semua tipe yang perlu casting)
+ protected function casts(): array
+ {
+ return [
+ 'status' => ProductStatus::class,
+ 'is_active' => 'boolean',
+ ];
+ }
+
+ // 2. Scopes (order abjad, gunakan atribut #[Scope])
+ #[Scope]
+ protected function active(Builder $query): void
+ {
+ $query->where('is_active', true);
+ }
+
+ #[Scope]
+ protected function draft(Builder $query): void
+ {
+ $query->where('status', ProductStatus::DRAFT);
+ }
+
+ // 3. Mutators & Accessors (order abjad, NAMA HARUS BERBEDA dari kolom DB)
+ // TIDAK perlu accessor untuk label — Enum sudah handle via ->label()
+ protected function formattedName(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => ucfirst($this->name),
+ );
+ }
+
+ // 4. Relationships (order abjad, WAJIB 2 ARAH)
+ public function categories(): BelongsToMany { ... }
+ public function productVariants(): HasMany { ... }
+}
+```
+
+### Casting — Wajib untuk Semua Tipe
+```php
+// ✅ Selalu cast kolom yang perlu — gunakan METHOD syntax
+protected function casts(): array
+{
+ return [
+ // Enum → gunakan enum class
+ 'status' => OrderStatus::class,
+ 'type' => CashTransactionType::class,
+
+ // Currency/angka → integer (tanpa desimal)
+ 'amount' => 'integer',
+ 'price' => 'integer',
+ 'quantity' => 'integer',
+
+ // Date → format Y-m-d
+ 'join_date' => 'date:Y-m-d',
+
+ // Boolean
+ 'is_active' => 'boolean',
+ 'is_affiliate' => 'boolean',
+
+ // Array (JSON)
+ 'marketplace_settings_snapshot' => 'array',
+ 'payload' => 'array',
+
+ // DateTime
+ 'verified_at' => 'datetime',
+ 'paid_at' => 'datetime',
+ ];
+}
+
+// ❌ JANGAN pakai property syntax
+protected $casts = [...]; // JANGAN
+
+// ❌ JANGAN biarkan kolom tanpa cast jika tipenya perlu
+'amount' => 'integer', // ✅ benar
+// 'amount' => '', // ❌ salah — tidak dicast
+```
+
+### Enum untuk Data Opsi
+```php
+// ✅ SELALU gunakan Enum untuk data yang punya opsi tetap
+enum OrderStatus: string
+{
+ use HasValues;
+
+ case PENDING = 'pending';
+ case PROCESSING = 'processing';
+ case COMPLETED = 'completed';
+ case CANCELLED = 'cancelled';
+ case REFUNDED = 'refunded';
+
+ public function label(): string
+ {
+ return match ($this) {
+ self::PENDING => 'Menunggu',
+ self::PROCESSING => 'Diproses',
+ self::COMPLETED => 'Selesai',
+ self::CANCELLED => 'Dibatalkan',
+ self::REFUNDED => 'Dikembalikan',
+ };
+ }
+}
+
+// ✅ JANGAN pakai string biasa untuk status
+// ❌ 'status' => 'pending' // tanpa enum
+// ✅ 'status' => OrderStatus::PENDING // dengan enum
+```
+
+### Accessor & Mutator — Nama harus BERBEDA dari kolom DB
+```php
+// ✅ Nama accessor TIDAK BOLEH sama dengan nama kolom di DB
+// Format: prefix 'formatted_' atau suffix '_label'
+// Gunakan Attribute::make(get: fn () => ...)
+
+use Illuminate\Database\Eloquent\Casts\Attribute;
+
+// Currency → prefix 'formatted_'
+protected function formattedAmount(): Attribute
+{
+ return Attribute::make(
+ get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
+ );
+}
+
+// Date → prefix 'formatted_'
+protected function formattedJoinDate(): Attribute
+{
+ return Attribute::make(
+ get: fn () => $this->join_date?->translatedFormat('l, d F Y'),
+ );
+}
+
+// String → prefix 'formatted_'
+protected function formattedName(): Attribute
+{
+ return Attribute::make(
+ get: fn () => ucfirst($this->name),
+ );
+}
+
+// Phone → prefix 'formatted_' (get + set)
+protected function formattedPhoneNumber(): Attribute
+{
+ return Attribute::make(
+ get: fn () => $this->phone_number
+ ? preg_replace('/(\d{4})(?=\d)/', '$1 ', $this->phone_number)
+ : null,
+ set: fn ($value) => preg_replace('/\s/', '', $value),
+ );
+}
+
+// Label → suffix '_label' (TIDAK pakai formatted_)
+// TIDAK PERLU accessor manual — Enum sudah punya method label()
+// Cukup panggil langsung: $this->status->label()
+// Tidak perlu buat statusLabel() accessor
+
+// ❌ JANGAN: amount() — bentrok dengan kolom 'amount'
+// ❌ JANGAN: name() — bentrok dengan kolom 'name'
+// ✅ BENAR: formattedAmount(), formattedName()
+```
+
+### Appends — Data yang Ditampilkan
+```php
+// ✅ Gunakan snake_case (sama dengan nama accessor)
+#[Appends(['formatted_amount', 'formatted_join_date'])]
+
+// ❌ JANGAN append kolom yang sudah ada di DB (hanya untuk accessor)
+// ❌ Appends(['amount']) // amount sudah ada di DB
+// ✅ Appends(['formatted_amount']) // formatted_amount adalah accessor
+```
+
+### Scopes — untuk Data Opsi
+```php
+// ✅ SELALU buat scope untuk data yang punya opsi
+// Gunakan atribut #[Scope] dan return type void
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Attributes\Scope;
+
+// Untuk enum
+#[Scope]
+protected function pending(Builder $query): void
+{
+ $query->where('status', OrderStatus::PENDING);
+}
+
+// Untuk boolean
+#[Scope]
+protected function active(Builder $query): void
+{
+ $query->where('is_active', true);
+}
+
+// ❌ JANGAN: public function pending(Builder $query): Builder
+// ❌ JANGAN: scopePending (pakai prefix 'scope')
+// ✅ BENAR: #[Scope] + protected function pending(Builder $query): void
+```
+
+### Relationships — WAJIB 2 ARAH
+```php
+// ✅ Setiap relasi HARUS ditulis di KEDUA model
+
+// Model: User
+public function userProfile(): HasOne
+{
+ return $this->hasOne(UserProfile::class);
+}
+
+// Model: UserProfile (2 ARAH!)
+public function user(): BelongsTo
+{
+ return $this->belongsTo(User::class);
+}
+
+// ✅ Contoh relasi 2 arah:
+// User ↔ UserProfile
+// User ↔ Employee
+// User ↔ CashAccount (created_by_id)
+// CashAccount ↔ CashTransaction
+// CashTransaction ↔ Expense (1:1)
+// Order ↔ OrderItem
+// Cutting ↔ CuttingMaterial
+// dll.
+
+// ❌ JANGAN hanya tulis di salah 1 model
+// Jika ada User::userProfile(), maka HARUS ada UserProfile::user()
+```
+
+### Relationship Naming
+```php
+// BelongsTo → singular
+user() // → BelongsTo User
+cashAccount() // → BelongsTo CashAccount
+
+// HasMany → plural
+orders() // → HasMany Order
+payrolls() // → HasMany Payroll
+
+// HasOne → singular
+userProfile() // → HasOne UserProfile
+employee() // → HasOne Employee
+
+// Custom FK → pastikan ada parameter
+createdCuttings() // → HasMany Cutting, 'created_by_id'
+submittedCuttings() // → HasMany Cutting, 'submitted_by_id'
+```
+
+### Eloquent Select & Eager Loading — Selalu Select yang Dibutuhkan
+```php
+// ✅ SELALU select kolom yang dibutuhkan saja — gunakan array syntax
+$orders = Order::select(['id', 'order_number', 'total_amount', 'status'])
+ ->with(['customer:id,name', 'createdBy:id,username'])
+ ->get();
+
+// ✅ Eager load relasi yang dibutuhkan
+$products = Product::with([
+ 'categories:id,name',
+ 'productVariants:id,product_id,name,stock',
+])->get();
+
+// ❌ JANGAN: Product::all() — load semua kolom + semua relasi
+// ❌ JANGAN: Order::with(['customer', 'createdBy', 'orderItems', 'cashTransaction'])->get() — terlalu banyak relasi
+
+// ✅ Pilih relasi yang benar-benar ditampilkan di view
+$orders = Order::select(['id', 'order_number', 'total_amount', 'status', 'customer_id', 'created_by_id'])
+ ->with([
+ 'customer:id,name',
+ 'createdBy:id,username',
+ ])
+ ->orderBy($sort, $direction)
+ ->paginate($perPage);
+```
+
+---
+
+## Controller
+
+### SELALU Gunakan Return Type
+```php
+// ✅ Return type wajib ada di SEMUA method — controller & service
+// ✅ Response untuk controller, void/array/bool untuk service
+```
+
+### ZONK — Tidak Ada Logic di Controller
+```php
+// ✅ Controller HANYA mengatur lalu lintas
+// Seluruh logic ADA DI SERVICE
+
+class ProductController extends Controller
+{
+ public function __construct(
+ private ProductService $service
+ ) {}
+
+ public function index(Request $request): Response
+ {
+ return Inertia::render('Admin/Master/Products/Index', [
+ 'products' => $this->service->paginated(
+ perPage: $request->input('per_page', 25),
+ search: $request->input('search', ''),
+ sort: $request->input('sort', 'created_at'),
+ direction: $request->input('direction', 'desc'),
+ ),
+ ]);
+ }
+
+ public function store(ProductRequest $request): Response
+ {
+ $this->service->store($request->validated());
+
+ Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil ditambahkan']);
+
+ return back();
+ }
+}
+
+// ❌ JANGAN ada logic di controller, SEKECIL APAPUN
+// ❌ $name = strtoupper($request->name); // logic → pindah ke service
+// ❌ if ($request->hasFile('photo')) { ... } // logic → pindah ke service
+
+// ✅ Jika ada yang看似simple seperti resetPassword, tetap pindah ke service
+public function resetPassword(UserRequest $request, User $user): Response
+{
+ $this->service->resetPassword($user, $request->validated());
+
+ Inertia::flash('toast', ['type' => 'success', 'message' => 'Password berhasil direset']);
+
+ return back();
+}
+```
+
+### Urutan Method
+```php
+class ProductController extends Controller
+{
+ // 1. __construct()
+ public function __construct(private ProductService $service) {}
+
+ // 2. index()
+ public function index(Request $request): Response { ... }
+
+ // 3. create()
+ public function create(): Response { ... }
+
+ // 4. store()
+ public function store(ProductRequest $request): Response { ... }
+
+ // 5. show() — jika ada
+ public function show(Product $product): Response { ... }
+
+ // 6. edit()
+ public function edit(Product $product): Response { ... }
+
+ // 7. update()
+ public function update(ProductRequest $request, Product $product): Response { ... }
+
+ // 8. destroy()
+ public function destroy(Product $product): Response { ... }
+
+ // 9. Custom actions (jika ada, JANGAN dipaksakan)
+ public function toggleStatus(Product $product): Response { ... }
+ public function resetPassword(UserRequest $request, User $user): Response { ... }
+}
+```
+
+### Return ke Halaman yang Sama — Gunakan `back()`
+```php
+// ✅ SELALU gunakan Inertia::flash() sebelum return back()
+public function destroy(Product $product)
+{
+ $this->service->destroy($product);
+
+ Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil dihapus']);
+
+ return back();
+}
+
+// ❌ JANGAN pakai ->with()
+return back()->with('success', 'Produk berhasil dihapus'); // JANGAN
+
+// ✅ Untuk error di handleAction
+return $this->handleAction(
+ fn () => $this->service->store($data),
+ 'Data berhasil ditambahkan',
+ route('admin.master.products.index')
+);
+// handleAction sudah handle flash otomatis
+
+// ✅ Untuk redirect ke halaman lain
+return to_route('dashboard'); // ✅ hanya jika memang pindah halaman
+```
+
+### Jangan Panggil Model Langsung — Gunakan Service
+```php
+// ❌ JANGAN panggil Model langsung di Controller
+public function index()
+{
+ $categories = Category::all(); // JANGAN
+ return Inertia::render('...', compact('categories'));
+}
+
+// ✅ SELALU gunakan Service
+public function index()
+{
+ $categories = $this->categoryService->getAll(); // ✅
+ return Inertia::render('...', compact('categories'));
+}
+```
+
+### DB Transaction — Gunakan `handleAction()` untuk 2+ Query
+```php
+// ✅ handleAction() → untuk 2+ query/operasi dalam 1 action
+public function store(OrderRequest $request): Response
+{
+ return $this->handleAction(
+ fn () => $this->service->store($request->validated()),
+ 'Transaksi berhasil ditambahkan',
+ route('admin.manage.transactions.index')
+ );
+}
+
+// ✅ handleAction() otomatis wrap dalam DB::transaction()
+// ✅ Jika gagal, otomatis rollback + flash error
+
+// ✅ 1 query → manual flash (tanpa handleAction)
+public function destroy(Product $product): Response
+{
+ $this->service->destroy($product);
+
+ Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil dihapus']);
+
+ return back();
+}
+```
+
+### Service Property — Constructor Promotion
+```php
+// ✅ Gunakan constructor promotion
+public function __construct(
+ private ProductService $service
+) {}
+
+// ❌ JANGAN
+private ProductService $service = new ProductService; // JANGAN
+public ProductService $service; // JANGAN (public)
+```
+
+---
+
+## Service
+
+### ZONK — Semua Logic Ada di Sini
+```php
+// ✅ Service adalah tempat SELURUH logic
+// Komunikasi DB, validasi bisnis, manipulasi data — semua di sini
+
+class ProductService
+{
+ public function store(array $data): Product
+ {
+ $data['slug'] = Str::slug($data['name']);
+ return Product::create($data);
+ }
+
+ public function update(Product $product, array $data): Product
+ {
+ $product->update($data);
+ return $product;
+ }
+
+ public function destroy(Product $product): void
+ {
+ $product->delete();
+ }
+
+ public function resetPassword(User $user, array $data): void
+ {
+ $user->update([
+ 'password' => Hash::make($data['password']),
+ ]);
+ }
+}
+```
+
+### Penamaan Method — HARUS KONSISTEN
+```php
+// ✅ Nama method harus KONSISTEN di SEMUA service
+// Semua service HARUS punya method ini:
+
+// 1. paginated() — untuk paginasi
+public function paginated(
+ int $perPage = 25,
+ string $search = '',
+ string $sort = 'created_at',
+ string $direction = 'desc',
+ array $filters = []
+): LengthAwarePaginator {
+ return Product::query()
+ ->select(['id', 'name', 'slug', 'status']) // ✅ select kolom yang dibutuhkan
+ ->with(['categories:id,name']) // ✅ eager load relasi
+ ->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
+ ->orderBy($sort, $direction)
+ ->paginate($perPage);
+}
+
+// 2. getAll() — untuk semua data (tanpa paginasi)
+public function getAll(array $filters = []): Collection
+{
+ return Product::query()
+ ->select(['id', 'name'])
+ ->get();
+}
+
+// 3. store() — untuk create
+public function store(array $data): Product
+{
+ return Product::create($data);
+}
+
+// 4. update() — untuk update
+public function update(Product $product, array $data): Product
+{
+ $product->update($data);
+ return $product;
+}
+
+// 5. destroy() — untuk delete
+public function destroy(Product $product): void
+{
+ $product->delete();
+}
+
+// ❌ JANGAN beda-beda nama untuk fungsi yang sama
+// ❌ getProducts(), fetchProducts(), listProducts()
+// ✅ paginated() — 1 nama untuk 1 fungsi
+```
+
+### Return Type — Wajib di Semua Method
+```php
+// ✅ Return type wajib ada di SEMUA method
+public function paginated(...): LengthAwarePaginator { ... }
+public function getAll(...): Collection { ... }
+public function store(array $data): Product { ... }
+public function update(Product $product, array $data): Product { ... }
+public function destroy(Product $product): void { ... }
+```
+
+### Property Declaration
+```php
+// ✅ Di service, property declaration seperti biasa (private readonly)
+class ProductService
+{
+ private readonly CuttingService $cuttingService;
+
+ public function __construct(CuttingService $cuttingService)
+ {
+ $this->cuttingService = $cuttingService;
+ }
+}
+```
+
+### Service Concerns (Traits)
+```php
+// ✅ Gunakan traits untuk kode yang berulang di banyak service
+
+// 1. HandlesCashTransactions — untuk operasi kas
+use App\Services\Concerns\HandlesCashTransactions;
+
+class ExpenseService
+{
+ use HandlesCashTransactions;
+
+ public function store(array $data): Expense
+ {
+ $cashAccount = $this->getCashAccount($data['cash_account_id']);
+ $this->debitCash($cashAccount, $data['amount'], $data['description']);
+ return Expense::create($data);
+ }
+}
+
+// 2. HasStockAdjustment — untuk operasi stok
+use App\Services\Concerns\HasStockAdjustment;
+
+class TransactionService
+{
+ use HasStockAdjustment;
+
+ public function store(array $data): Order
+ {
+ $this->applyStock($data['items'], $data['stock_type'], -1); // kurangi stok
+ return Order::create($data);
+ }
+}
+
+// 3. RegistersMedia — untuk upload file
+use App\Services\Concerns\RegistersMedia;
+
+class RestockService
+{
+ use RegistersMedia;
+
+ public function store(array $data): Restock
+ {
+ $restock = Restock::create($data);
+ $this->registerMedia($restock, $data['photo'] ?? null, 'restock');
+ return $restock;
+ }
+}
+```
+
+### Cash Transaction Pattern
+```php
+// Credit (DEPOSIT) → tambah saldo
+$this->creditCash($cashAccount, $amount, $description);
+
+// Debit (EXPENSE/WITHDRAWAL) → kurangi saldo + validasi
+$this->debitCash($cashAccount, $amount, $description);
+```
+
+### Stock Adjustment Pattern
+```php
+// ProductVariant — increment/decrement stock
+$this->applyStock($items, $stockType, $sign); // $sign: 1 (tambah) atau -1 (kurangi)
+
+// RawMaterialPrice — increment/decrement
+$this->adjustStock($model, $field, $quantity, $sign);
+```
+
+---
+
+## Form Request
+
+### Struktur Dasar
+```php
+class ProductRequest extends FormRequest
+{
+ use CurrencyStripping;
+
+ public function authorize(): bool
+ {
+ return true; // authorization di controller/middleware
+ }
+
+ public function rules(): array
+ {
+ return [
+ 'name' => ['required', 'string', 'max:200'],
+ 'price' => ['required', 'integer'],
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'name' => 'Nama Produk',
+ 'price' => 'Harga',
+ ];
+ }
+
+ public function prepareForValidation(): void
+ {
+ $this->merge($this->stripCurrencyDot($this->validated(), 'price', 'discount'));
+ }
+}
+```
+
+### Currency Stripping
+```php
+use App\Concerns\CurrencyStripping;
+
+// ✅ Strip dot dari input Rupiah sebelum validasi
+// Input: "1.000.000" → Output: 1000000
+$this->merge($this->stripCurrencyDot($this->validated(), 'price', 'discount'));
+
+// ✅ Untuk nested array
+$this->merge($this->stripCurrencyDot($this->validated(), 'variants.*.price'));
+```
+
+### Unique Ignore
+```php
+// ✅ Saat update, ignore ID sendiri
+Rule::unique('products')->ignore($this->route('product')?->id)
+
+// ✅ Saat store, tidak perlu ignore
+Rule::unique('products')
+```
+
+### Shared Store/Update Request
+```php
+// ✅ Gunakan 1 request dengan sometimes untuk store & update
+public function rules(): array
+{
+ $productId = $this->route('product')?->id;
+
+ return [
+ 'name' => ['required', 'string', 'max:200', Rule::unique('products')->ignore($productId)],
+ 'price' => ['required', 'integer'],
+ 'description' => ['nullable', 'string'],
+ ];
+}
+```
+
+---
+
+## Traits & Concerns — Reuse Kode yang Berulang
+
+```php
+// ✅ SELALU cari kode yang berulang, jadikan Trait/Concern
+
+// Contoh: currency formatting berulang di banyak model
+// ✅ Buat Trait
+namespace App\Concerns;
+
+trait CurrencyFormatting
+{
+ protected function formatCurrency($value): string
+ {
+ return 'Rp ' . number_format($value, 0, ',', '.');
+ }
+}
+
+// ✅ Gunakan di model
+use App\Concerns\CurrencyFormatting;
+
+#[Guarded(['id'])]
+#[Appends(['formatted_amount'])]
+class Order extends Model
+{
+ use HasFactory, SoftDeletes, CurrencyFormatting;
+
+ protected function formattedAmount(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->formatCurrency($this->amount),
+ );
+ }
+}
+
+// ✅ Contoh Traits yang sudah ada:
+// - CurrencyStripping → untuk FormRequest
+// - HandlesCashTransactions → untuk Service
+// - HasStockAdjustment → untuk Service
+// - RegistersMedia → untuk Service
+// - HasValues → untuk Enum
+
+// ✅ Jika menemukan pola berulang, buat Trait baru:
+// - SoftDeletesScope (untuk scope yang sama di banyak model)
+// - HasFormattedDates (untuk accessor tanggal)
+// - HasFormattedCurrency (untuk accessor currency)
+```
+
+---
+
+## Frontend (React + TypeScript + Inertia)
+
+### Page Patterns
+
+#### A. Simple CRUD Index (DataTable)
+Digunakan untuk: Category, Customer, Supplier, Employee, Leave Request, Expense, Cash Account
+
+```tsx
+import { Head } from '@inertiajs/react';
+import { useState } from 'react';
+import { useCan } from '@/hooks/use-can';
+import { useServerTable } from '@/hooks/use-server-table';
+import { PageHeader } from '@/components/page-header';
+import { DataTable } from '@/components/data-table';
+import { FormDialog } from '@/components/form-dialog';
+import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
+import { Button } from '@/components/ui/button';
+import { createCategoryColumns, type Category } from './columns';
+
+// Props dari controller (Inertia render)
+type Props = {
+ categories: { data: Category[]; current_page: number; last_page: number; per_page: number; total: number };
+};
+
+export default function CategoryIndex({ categories }: Props) {
+ const { can } = useCan();
+ const [createOpen, setCreateOpen] = useStateTambah Transaksi
+
+