From 4517eeb7eedb7e38d2e02f318de117b782b20c96 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 4 Aug 2026 23:03:56 +0700 Subject: [PATCH] feat: enhance employee and leave request management - Added roles property to Employee type and a new column for displaying employee roles in the employee table. - Updated leave request columns to use formatted start and end dates instead of raw date strings. - Enhanced leave request index to accept filter options for status dynamically. - Refactored transaction index to support dynamic filter options for status, channel, and payment type. - Introduced AGENTS.md for session notes detailing model relationships, casting, scopes, reorganizations, and conventions. --- AGENTS.md | 957 ++++++++++++++++++ app/Enums/Concerns/HasValues.php | 13 + .../Admin/Finance/CashAccountController.php | 4 + .../Admin/HR/LeaveRequestController.php | 4 + app/Models/CashTransaction.php | 16 +- app/Models/EmployeeAdvance.php | 9 +- app/Models/Expense.php | 9 +- app/Services/Admin/HR/EmployeeService.php | 36 +- .../Admin/Manage/TransactionService.php | 9 +- resources/js/hooks/use-cutting-draft.ts | 24 +- resources/js/hooks/use-draft-save.ts | 21 + resources/js/hooks/use-product-draft.ts | 24 +- resources/js/hooks/use-purchase-draft.ts | 22 +- resources/js/hooks/use-raw-material-draft.ts | 24 +- resources/js/hooks/use-restock-draft.ts | 22 +- resources/js/hooks/use-transaction-draft.ts | 22 +- resources/js/lib/format.ts | 46 +- .../admin/finance/cash-account/columns.tsx | 6 +- .../admin/finance/cash-account/index.tsx | 18 +- .../cash-account/transaction-columns.tsx | 17 +- .../finance/employee-advance/columns.tsx | 18 +- .../pages/admin/finance/expense/columns.tsx | 12 +- .../finance/payroll-period/show-columns.tsx | 24 +- .../js/pages/admin/hr/employee/columns.tsx | 15 + .../pages/admin/hr/leave-request/columns.tsx | 11 +- .../js/pages/admin/hr/leave-request/index.tsx | 19 +- .../pages/admin/manage/transaction/index.tsx | 35 +- 27 files changed, 1224 insertions(+), 213 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7ccfc30 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,957 @@ +# AGENTS.md - Session Notes + +## Status: In Progress + +--- + +## 1. Relasi Dua Arah - Perubahan di Sesi Ini + +| Model | Relasi Baru | Tipe | Inverse | +|-------|------------|------|---------| +| Cutting | submittedBy() | belongsTo(User, 'submitted_by_id') | User.submittedCuttings() | +| CashTransaction | employeeAdvances() | hasMany(EmployeeAdvance) | EmployeeAdvance.cashTransaction() | +| CashTransaction | payrolls() | hasMany(Payroll) | Payroll.cashTransaction() | +| Attendance | payrollAdjustments() | hasMany(PayrollAdjustment) | PayrollAdjustment.attendance() | +| User | cuttingMaterials() | hasMany(CuttingMaterial) | CuttingMaterial.user() | +| User | cuttingMaterialCombinations() | hasMany(CuttingMaterialCombination) | CuttingMaterialCombination.user() | +| User | cuttingResults() | hasMany(CuttingResult) | CuttingResult.user() | +| User | pushSubscriptions() | hasMany(PushSubscription) | PushSubscription.user() | + +### File diubah untuk relasi: +- app/Models/Cutting.php - tambah submittedBy() +- app/Models/CashTransaction.php - tambah employeeAdvances(), payrolls(), import HasMany +- app/Models/Attendance.php - tambah payrollAdjustments(), import HasMany +- app/Models/User.php - tambah 4 relasi + +--- + +## 2. Casting - Perubahan di Sesi Ini + +| Model | Kolom | Cast | Keterangan | +|-------|-------|------|------------| +| Cutting | sewing_cost | integer | tambah ke casts existing | +| Cutting | other_cost | integer | tambah ke casts existing | +| OwnerVerificationRequest | payload | array | tambah ke casts existing | +| Order | marketplace_settings_snapshot | array | tambah ke casts existing | +| ProductVariant | stock | integer | casts() baru dibuat | +| ProductVariant | reject_stock | integer | casts() baru dibuat | +| ProductVariant | retail_stock | integer | casts() baru dibuat | +| PayrollPeriod | year | integer | tambah ke casts existing | +| PayrollPeriod | month | integer | tambah ke casts existing | + +### File diubah untuk casting: +- app/Models/Cutting.php +- app/Models/OwnerVerificationRequest.php +- app/Models/Order.php +- app/Models/ProductVariant.php +- app/Models/PayrollPeriod.php + +--- + +## 3. Scope (Ordered by Abjad) - Perubahan di Sesi Ini + +### Model yang DITAMBAH scope-nya: +| Model | Scope Baru | Enum | +|-------|-----------|------| +| Cutting | cancelled(), completed(), inProgress() | CuttingStatus | +| RawMaterial | kg(), meter(), yard() | RawMaterialUnit | + +### Model yang SUDAH LENGKAP scopes: +| Model | Scopes (abjad) | +|-------|---------------| +| CashTransaction | deposit, expenseType, transfer, withdrawal | +| Employee | contract, fullTime, internship, partTime, resigned | +| EmployeeAdvance | approved, cancelled, paid, pending, rejected | +| LeaveRequest | approved, cancelled, pending, rejected | +| Order | cancelled, cash, completed, pending, processing, qris, refunded, retail, shopee, store, tiktok, transfer, wholesale | +| OrderItem | good, reject | +| Payroll | cancelled, paid, unpaid | +| PayrollAdjustment | bonus, deduction | +| PayrollPeriod | closed, open | +| Product | active, draft, inactive | +| ProductPrice | retail, wholesale | +| Restock | good, reject | +| StokOpname | cancelled, completed, draft, inProgress, verified | +| StokOpnameItem | good, reject | +| UserProfile | female, hasPhoneNumber, male | +| User | active | + +### Model yang TIDAK punya enum (tidak perlu scope): +AppNotification, CashAccount, Category, Customer, Expense, CuttingMaterial, CuttingMaterialCombination, CuttingResult, OwnerVerificationRequest, ProductCategory, ProductVariant, Purchase, PurchaseItem, RawMaterialPrice, Rejection, RestockItem, RetailStockHistory, StockMutation, Supplier, SystemConfiguration, HomepageConfiguration, PushSubscription + +--- + +## 4. Reorganisasi Model - Semua model diubah urutannya menjadi: +**casts → scopes (abjad) → relations (abjad)** + +### Semua 41 model sudah di-reorganize di sesi ini. + +--- + +## 5. Relasi Dua Arah - Perubahan di Sesi Ini + +### AppNotification +- belongsTo User (user_id) + +### Attendance +- belongsTo Employee (employee_id) +- hasMany PayrollAdjustment + +### CashAccount +- belongsTo User (created_by_id) +- hasMany CashTransaction + +### CashTransaction +- belongsTo CashAccount (cash_account_id) +- belongsTo User (created_by_id) +- hasMany EmployeeAdvance +- hasMany Payroll +- hasOne Expense +- hasOne Order +- morphTo Reference (reference_id, reference_type) + +### Category +- belongsToMany Product (via product_categories) + +### Cutting +- belongsTo User (created_by_id) +- belongsTo User (submitted_by_id) +- hasMany CuttingMaterialCombination +- hasMany CuttingMaterial +- hasMany CuttingResult + +### CuttingMaterial +- belongsTo CuttingMaterialCombination (combination_id) +- belongsTo Cutting (cutting_id) +- belongsTo RawMaterialPrice (raw_material_price_id) +- belongsTo User (user_id) + +### CuttingMaterialCombination +- belongsTo Cutting (cutting_id) +- hasMany CuttingMaterial (combination_id) +- belongsTo User (user_id) + +### CuttingResult +- belongsTo Cutting (cutting_id) +- belongsTo User (user_id) + +### Customer +- hasMany Order (customer_id) + +### Employee +- belongsTo User (user_id) +- hasMany Attendance +- hasMany EmployeeAdvance +- hasMany LeaveRequest +- hasMany Payroll + +### EmployeeAdvance +- belongsTo CashTransaction (cash_transaction_id) +- belongsTo Employee (employee_id) +- belongsTo User (paid_by_id) +- belongsTo CashTransaction (repayment_cash_transaction_id) +- belongsTo User (verified_by_id) + +### Expense +- belongsTo CashTransaction (cash_transaction_id) +- belongsTo User (created_by_id) + +### LeaveRequest +- belongsTo Employee (employee_id) +- belongsTo User (verified_by_id) + +### Order +- belongsTo CashTransaction (cash_transaction_id) +- belongsTo User (created_by_id) +- belongsTo Customer (customer_id) +- belongsTo User (marketing_id) +- hasMany OrderItem (order_id) + +### OrderItem +- belongsTo Order (order_id) +- belongsTo ProductVariant (product_variant_id) +- belongsTo User (user_id) + +### OwnerVerificationRequest +- morphTo Subject (subject_id, subject_type) +- belongsTo User (submitted_by_id) +- belongsTo User (verified_by_id) + +### Payroll +- belongsTo CashTransaction (cash_transaction_id) +- belongsTo Employee (employee_id) +- belongsTo User (paid_by_id) +- hasMany PayrollAdjustment (payroll_id) +- belongsTo PayrollPeriod (payroll_period_id) + +### PayrollAdjustment +- belongsTo Attendance (attendance_id) +- belongsTo User (created_by_id) +- belongsTo Payroll (payroll_id) + +### PayrollPeriod +- belongsTo User (closed_by_id) +- hasMany Payroll (payroll_period_id) + +### Product +- belongsToMany Category (via product_categories) +- hasMany ProductVariant (product_id) + +### ProductCategory (Pivot) +- belongsTo Category (category_id) +- belongsTo Product (product_id) + +### ProductPrice +- belongsTo ProductVariant (variant_id) + +### ProductVariant +- belongsTo Product (product_id) +- hasMany OrderItem (product_variant_id) +- hasMany ProductPrice (variant_id) +- hasMany RestockItem (product_variant_id) +- hasMany RetailStockHistory (product_variant_id) +- hasMany StokOpnameItem (product_variant_id) +- hasMany StockMutation (stockable_id, polymorphic) + +### Purchase +- belongsTo User (created_by_id) +- hasMany PurchaseItem (purchase_id) +- belongsTo Supplier (supplier_id) + +### PurchaseItem +- belongsTo Purchase (purchase_id) +- belongsTo RawMaterialPrice (raw_material_price_id) +- belongsTo User (user_id) + +### PushSubscription +- belongsTo User (user_id) + +### RawMaterial +- hasMany RawMaterialPrice (raw_material_id) + +### RawMaterialPrice +- belongsTo RawMaterial (raw_material_id) +- hasMany CuttingMaterial (raw_material_price_id) +- hasMany PurchaseItem (raw_material_price_id) + +### Rejection +- morphTo Rejectable (rejectable_id, rejectable_type) +- belongsTo User (rejected_by_id) + +### Restock +- belongsTo User (created_by_id) +- hasMany RestockItem (restock_id) + +### RestockItem +- belongsTo ProductVariant (product_variant_id) +- belongsTo Restock (restock_id) +- belongsTo User (user_id) + +### RetailStockHistory +- belongsTo ProductVariant (product_variant_id) +- belongsTo User (user_id) + +### StockMutation +- morphTo Source (source_id, source_type) +- morphTo Stockable (stockable_id, stockable_type) +- belongsTo User (user_id) + +### StokOpname +- belongsTo User (created_by_id) +- hasMany StokOpnameItem (stok_opname_id) +- belongsTo User (verified_by_id) + +### StokOpnameItem +- belongsTo ProductVariant (product_variant_id) +- belongsTo StokOpname (stok_opname_id) + +### Supplier +- hasMany Purchase (supplier_id) + +### User +- hasMany Attendance (user_id) +- hasMany CashAccount (created_by_id) +- hasMany CashTransaction (created_by_id) +- hasMany Cutting (created_by_id) +- hasMany Cutting (submitted_by_id) +- hasMany CuttingMaterialCombination (user_id) +- hasMany CuttingMaterial (user_id) +- hasMany CuttingResult (user_id) +- hasMany Expense (created_by_id) +- hasMany Order (created_by_id) +- hasMany Order (marketing_id) +- hasMany OrderItem (user_id) +- hasMany OwnerVerificationRequest (submitted_by_id) +- hasMany OwnerVerificationRequest (verified_by_id) +- hasMany Payroll (paid_by_id) +- hasMany PayrollAdjustment (created_by_id) +- hasMany PayrollPeriod (closed_by_id) +- hasMany Purchase (created_by_id) +- hasMany PurchaseItem (user_id) +- hasMany PushSubscription (user_id) +- hasMany Rejection (rejected_by_id) +- hasMany Restock (created_by_id) +- hasMany RestockItem (user_id) +- hasMany RetailStockHistory (user_id) +- hasMany StockMutation (user_id) +- hasMany StokOpname (created_by_id) +- hasMany StokOpname (verified_by_id) +- hasOne Employee (user_id) +- hasOne UserProfile (user_id) + +### UserProfile +- belongsTo User (user_id) + +--- + +## 6. Complete Casting Map + +### Attendance +- attendance_date -> date:Y-m-d, check_in_at -> datetime, check_out_at -> datetime +- check_in_latitude -> decimal:7, check_in_longitude -> decimal:7 +- check_out_latitude -> decimal:7, check_out_longitude -> decimal:7 +- work_duration_minutes -> integer + +### CashAccount +- balance -> integer + +### CashTransaction +- type -> CashTransactionType enum, amount -> integer, balance_after -> integer + +### Cutting +- status -> CuttingStatus enum, total_material_cost -> integer, cost_per_unit -> integer +- sewing_cost -> integer, other_cost -> integer + +### CuttingMaterial +- material_usage -> integer, material_result -> integer + +### CuttingMaterialCombination +- material_result -> integer + +### CuttingResult +- cutting_result -> integer, sample -> integer, original_outside_sample -> integer + +### Employee +- employment_status -> EmploymentStatus enum, base_salary -> integer +- join_date -> date:Y-m-d, resign_date -> date:Y-m-d + +### EmployeeAdvance +- status -> EmployeeAdvanceStatus enum, amount -> integer, paid_amount -> integer +- due_date -> date:Y-m-d, verified_at -> datetime, paid_at -> datetime + +### Order +- channel -> OrderChannel enum, price_type -> PriceType enum +- status -> OrderStatus enum, payment_type -> PaymentType enum +- is_affiliate -> boolean, subtotal -> integer, discount -> integer +- nego_price -> integer, total_amount -> integer, cogs -> integer +- marketplace_settings_snapshot -> array + +### OrderItem +- stock_quality -> ProductStockQuality enum, quantity -> integer +- unit_price -> integer, subtotal -> integer + +### OwnerVerificationRequest +- payload -> array, verified_at -> datetime + +### Payroll +- status -> PayrollStatus enum, base_salary -> integer +- bonus_amount -> integer, deduction_amount -> integer +- total_amount -> integer, paid_at -> datetime + +### PayrollAdjustment +- type -> PayrollAdjustmentType enum, amount -> integer + +### PayrollPeriod +- status -> PayrollPeriodStatus enum, year -> integer +- month -> integer, closed_at -> datetime + +### Product +- status -> ProductStatus enum + +### ProductPrice +- type -> PriceType enum, price -> integer + +### ProductVariant +- stock -> integer, reject_stock -> integer, retail_stock -> integer + +### RawMaterial +- unit -> RawMaterialUnit enum, is_active -> boolean + +### RawMaterialPrice +- price -> integer, stock -> integer + +### User +- email_verified_at -> datetime, is_active -> boolean +- last_login_at -> datetime, password -> hashed +- two_factor_confirmed_at -> datetime + +--- + +## 5. Migration Foreign Keys + +| Table | FK Column | References | +|-------|-----------|------------| +| user_profiles | user_id | users(id) | +| employees | user_id | users(id) | +| product_categories | product_id | products(id) | +| product_categories | category_id | categories(id) | +| product_variants | product_id | products(id) | +| raw_material_prices | raw_material_id | raw_materials(id) | +| cash_accounts | created_by_id | users(id) | +| cash_transactions | cash_account_id | cash_accounts(id) | +| cash_transactions | created_by_id | users(id) | +| expenses | cash_transaction_id | cash_transactions(id) | +| expenses | created_by_id | users(id) | +| employee_advances | paid_by_id | users(id) | +| employee_advances | employee_id | employees(id) | +| employee_advances | verified_by_id | users(id) | +| employee_advances | repayment_cash_transaction_id | cash_transactions(id) | +| employee_advances | cash_transaction_id | cash_transactions(id) | +| rejections | rejected_by_id | users(id) | +| attendances | employee_id | employees(id) | +| payroll_periods | closed_by_id | users(id) | +| payrolls | payroll_period_id | payroll_periods(id) | +| payrolls | employee_id | employees(id) | +| payrolls | cash_transaction_id | cash_transactions(id) | +| payrolls | paid_by_id | users(id) | +| payroll_adjustments | payroll_id | payrolls(id) | +| payroll_adjustments | attendance_id | attendances(id) | +| payroll_adjustments | created_by_id | users(id) | +| leave_requests | employee_id | employees(id) | +| leave_requests | verified_by_id | users(id) | +| purchases | supplier_id | suppliers(id) | +| purchases | created_by_id | users(id) | +| purchase_items | purchase_id | purchases(id) | +| purchase_items | user_id | users(id) | +| purchase_items | raw_material_price_id | raw_material_prices(id) | +| orders | customer_id | customers(id) | +| orders | marketing_id | users(id) | +| orders | cash_transaction_id | cash_transactions(id) | +| orders | created_by_id | users(id) | +| order_items | order_id | orders(id) | +| order_items | user_id | users(id) | +| order_items | product_variant_id | product_variants(id) | +| cuttings | created_by_id | users(id) | +| cuttings | submitted_by_id | users(id) | +| cutting_material_combinations | user_id | users(id) | +| cutting_material_combinations | cutting_id | cuttings(id) | +| cutting_materials | user_id | users(id) | +| cutting_materials | cutting_id | cuttings(id) | +| cutting_materials | raw_material_price_id | raw_material_prices(id) | +| cutting_materials | combination_id | cutting_material_combinations(id) | +| cutting_results | user_id | users(id) | +| cutting_results | cutting_id | cuttings(id) | +| product_prices | variant_id | product_variants(id) | +| owner_verification_requests | submitted_by_id | users(id) | +| owner_verification_requests | verified_by_id | users(id) | +| notifications | user_id | users(id) | +| employee_advance_payments | employee_advance_id | employee_advances(id) | +| employee_advance_payments | paid_by_id | users(id) | +| employee_advance_payments | cash_transaction_id | cash_transactions(id) | +| retail_stock_histories | product_variant_id | product_variants(id) | +| retail_stock_histories | user_id | users(id) | +| stok_opnames | created_by_id | users(id) | +| stok_opnames | verified_by_id | users(id) | +| stok_opname_items | stok_opname_id | stok_opnames(id) | +| stok_opname_items | product_variant_id | product_variants(id) | +| restocks | created_by_id | users(id) | +| restock_items | restock_id | restocks(id) | +| restock_items | user_id | users(id) | +| restock_items | product_variant_id | product_variants(id) | +| stock_mutations | user_id | users(id) | + +--- + +## 8. Accessor & Mutator - Perubahan di Sesi Ini + +### A. Currency Format (Get: `Rp X.XXX`) +| Model | Kolom | +|-------|-------| +| CashAccount | balance | +| CashTransaction | amount | +| Cutting | total_material_cost, cost_per_unit, sewing_cost, other_cost | +| Employee | base_salary | +| EmployeeAdvance | amount, paid_amount | +| Expense | amount | +| Order | subtotal, discount, nego_price, total_amount, cogs | +| OrderItem | unit_price, subtotal | +| Payroll | base_salary, bonus_amount, deduction_amount, total_amount | +| PayrollAdjustment | amount | +| ProductPrice | price | +| Purchase | subtotal, discount, shipping_cost, total | +| PurchaseItem | unit_price, subtotal | +| RawMaterialPrice | price | +| Restock | subtotal, total | +| RestockItem | unit_price, subtotal | + +### B. Phone Format `0821 2121 2121` (Get/Set) +| Model | Kolom | +|-------|-------| +| UserProfile | phone_number | +| Customer | phone_number | +| Supplier | phone_number | + +### C. String Format (Get: ucfirst) +| Model | Kolom | +|-------|-------| +| CashAccount | name | +| Category | name | +| Customer | name | +| Product | name | +| ProductVariant | name | +| RawMaterial | name | +| Supplier | name | +| UserProfile | first_name, last_name | + +### D. Full Name (Computed) +| Model | Kolom | Get | +|-------|-------|-----| +| UserProfile | full_name | first_name . ' ' . last_name | +| User | fullName | userProfile full_name atau username | + +### E. Email (Set: lowercase) +| Model | Kolom | Set | +|-------|-------|-----| +| User | email | strtolower | + +### F. Date Format Indonesia (Get: `l, d F Y`) +| Model | Kolom | +|-------|-------| +| Attendance | attendance_date | +| Employee | join_date, resign_date | +| EmployeeAdvance | due_date | +| LeaveRequest | start_date, end_date | +| StokOpname | opname_date | +| UserProfile | birth_date | + +### G. Status Label (Get: `->label()`) +| Model | Kolom | +|-------|-------| +| CashTransaction | typeLabel (type) | +| Cutting | statusLabel (status) | +| Employee | employmentStatusLabel (employment_status) | +| EmployeeAdvance | statusLabel (status) | +| LeaveRequest | statusLabel (status) | +| Order | statusLabel (status), channelLabel (channel), paymentTypeLabel (payment_type) | +| OrderItem | stockQualityLabel (stock_quality) | +| Payroll | statusLabel (status) | +| PayrollAdjustment | typeLabel (type) | +| PayrollPeriod | statusLabel (status) | +| Product | statusLabel (status) | +| ProductPrice | typeLabel (type) | +| RawMaterial | unitLabel (unit) | +| Restock | stockTypeLabel (stock_type) | +| StokOpname | statusLabel (status) | +| StokOpnameItem | stockQualityLabel (stock_quality) | +| UserProfile | genderLabel (gender) | + +--- + +## 9. Enum Label - Perubahan di Sesi Ini + +### Enum yang ditambah `label()` method: +| Enum | Labels | +|------|--------| +| CashTransactionType | Setoran, Pengeluaran, Transfer, Penarikan | +| CuttingStatus | Dibatalkan, Selesai, Dalam Proses | +| EmployeeAdvanceStatus | Disetujui, Dibatalkan, Dibayar, Menunggu, Ditolak | +| EmploymentStatus | Kontrak, Penuh Waktu, Magang, Paruh Waktu, Keluar | +| Gender | Perempuan, Laki-laki | +| LeaveRequestStatus | Disetujui, Dibatalkan, Menunggu, Ditolak | +| PayrollAdjustmentType | Bonus, Potongan | +| PayrollPeriodStatus | Tutup, Buka | +| PayrollStatus | Dibatalkan, Dibayar, Belum Dibayar | +| RawMaterialUnit | Kg, Meter, Yard | +| StokOpnameStatus | Dibatalkan, Selesai, Draft, Dalam Proses, Terverifikasi | + +### Enum yang SUDAH punya `label()`: +OrderChannel, OrderStatus, PaymentType, PriceType, ProductStatus, ProductStockQuality + +### HasValues Trait - `toSelect()` Method +Semua enum yang pakai `HasValues` trait bisa panggil `EnumName::toSelect()` untuk return `Collection`. + +```php +// Sebelum ( verbose ): +collect(OrderStatus::cases())->map(fn ($s) => ['value' => $s->value, 'label' => $s->label()])->values() + +// Sesudah ( clean ): +OrderStatus::toSelect() + +// Dengan filter: +PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values() +``` + +**File yang sudah di-refactor:** +- `app/Http/Controllers/Admin/Finance/CashAccountController.php` - `CashTransactionType::toSelect()` +- `app/Http/Controllers/Admin/HR/LeaveRequestController.php` - `LeaveRequestStatus::toSelect()` +- `app/Services/Admin/Manage/TransactionService.php` - `OrderStatus::toSelect()`, `OrderChannel::toSelect()`, `PaymentType::toSelect()`, `PriceType::toSelect()` + +--- + +## 10. Appends - Perubahan di Sesi Ini + +### Catatan Penting: +- **Accessor tidak boleh sama nama dengan kolom database** → gunakan prefix `formatted_` untuk menghindari bentrok +- Label accessors (status_label, type_label, dll) tidak bentrok karena bukan kolom DB + +### Model yang ditambah `#[Appends([...])]`: + +| Model | Appends | +|-------|---------| +| CashAccount | `['formatted_balance', 'formatted_name']` | +| CashTransaction | `['formatted_amount', 'formatted_balance_after', 'formatted_created_at', 'type_label']` | +| Category | `['formatted_name']` | +| Cutting | `['formatted_cost_per_unit', 'formatted_other_cost', 'formatted_sewing_cost', 'status_label', 'formatted_total_material_cost']` | +| Customer | `['formatted_name', 'formatted_phone_number']` | +| Employee | `['formatted_base_salary', 'employment_status_label', 'formatted_join_date', 'formatted_resign_date']` | +| EmployeeAdvance | `['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label']` | +| Expense | `['formatted_amount', 'formatted_created_at', 'formatted_date']` | +| LeaveRequest | `['formatted_end_date', 'formatted_start_date', 'status_label']` | +| Order | `['channel_label', 'formatted_cogs', 'formatted_discount', 'formatted_nego_price', 'payment_type_label', 'status_label', 'formatted_subtotal', 'formatted_total_amount']` | +| OrderItem | `['stock_quality_label', 'formatted_subtotal', 'formatted_unit_price']` | +| Payroll | `['formatted_base_salary', 'formatted_bonus_amount', 'formatted_deduction_amount', 'status_label', 'formatted_total_amount']` | +| PayrollAdjustment | `['formatted_amount', 'type_label']` | +| PayrollPeriod | `['status_label']` | +| Product | `['formatted_name', 'status_label']` | +| ProductPrice | `['formatted_price', 'type_label']` | +| ProductVariant | `['formatted_name']` | +| Purchase | `['formatted_discount', 'formatted_shipping_cost', 'formatted_subtotal', 'formatted_total']` | +| PurchaseItem | `['formatted_subtotal', 'formatted_unit_price']` | +| RawMaterial | `['formatted_name', 'unit_label']` | +| RawMaterialPrice | `['formatted_price']` | +| Restock | `['stock_type_label', 'formatted_subtotal', 'formatted_total']` | +| RestockItem | `['formatted_subtotal', 'formatted_unit_price']` | +| StokOpname | `['formatted_opname_date', 'status_label']` | +| StokOpnameItem | `['stock_quality_label']` | +| Supplier | `['formatted_name', 'formatted_phone_number']` | +| UserProfile | `['formatted_birth_date', 'gender_label', 'formatted_phone_number']` | +| User | `['full_name', 'name']` | + +### Yang TIDAK di-append: +- `User.email` - set-only accessor (tidak ada get) + +### Aturan Penamaan Accessor: +| Tipe | Lama (BENTROK) | Baru (AMAN) | +|------|---------------|-------------| +| Currency | `amount()`, `price()`, `subtotal()` | `formattedAmount()`, `formattedPrice()`, `formattedSubtotal()` | +| Date | `joinDate()`, `dueDate()` | `formattedJoinDate()`, `formattedDueDate()` | +| String | `name()` | `formattedName()` | +| Phone | `phoneNumber()` | `formattedPhoneNumber()` | +| Label | `statusLabel()`, `typeLabel()` | TIDAK berubah (tidak bentrok) | + +### Aturan Accessor Pattern: +- **Gunakan `$this->`** untuk mengakses attribute, bukan parameter `$value` +- Contoh: `get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.')` +- Null-safe: `get: fn () => $this->join_date?->translatedFormat('l, d F Y')` + +--- + +## 11. Controller Conventions - Perubahan di Sesi Ini + +### Base Controller (`Controller.php`) +- `handleAction(callable $action, string $successMessage, string $redirectRoute, ?string $errorRoute = null, array $parameters = []): RedirectResponse` +- `handleToggle(callable $action, string $successMessage, string $redirectRoute): RedirectResponse` + +### Aturan Penamaan Controller: +| Pattern | Keterangan | +|---------|------------| +| `handleAction()` | Untuk 2+ query/operasi dalam 1 action (try-catch) | +| `handleToggle()` | Untuk toggle operations (simple, non-dynamic message) | +| Manual flash | Untuk 1 query/operasi (single query) | + +### Controller Property: +- Service properties harus `private readonly XService $service` + +### Method Ordering: +1. `__construct` +2. `index` +3. `show` (jika ada) +4. `create` +5. `store` +6. `edit` +7. `update` +8. `destroy` +9. Custom actions (toggleStatus, approve, reject, dll) + +### Perubahan di Sesi Ini: + +| Controller | Perubahan | +|------------|-----------| +| StockMutationController | Fix inline instantiation → `private readonly StockMutationService $service` | +| ProfileController | Manual flash → `handleAction()` (2 queries: user save + profile updateOrCreate) | +| 23 controllers | Tambah `readonly` ke service properties | + +### Catatan Penting: +- **`handleAction()` hanya untuk 2+ query/operasi** dalam 1 action +- **1 query = manual flash** (lebih simpel, tidak perlu try-catch overhead) +- Contoh 2+ query: `ProfileController::update()` → user save + profile updateOrCreate + +--- + +## 12. Service Conventions - Perubahan di Sesi Ini + +### Traits (Shared Concerns) + +| Trait | Methods | Digunakan Oleh | +|-------|---------|----------------| +| `HandlesCashTransactions` | `getCashAccount()`, `creditCash()`, `debitCash()` | CashAccountService, ExpenseService, EmployeeAdvanceService, PayrollPeriodService | +| `HasStockAdjustment` | `adjustStock()`, `adjustVariantStock()`, `applyStock()`, `reverseStock()` | TransactionService, RestockService | +| `RegistersMedia` | `registerMedia()`, `syncPhoto()` | CashAccountService, CuttingService, ExpenseService, PurchaseService, RestockService, TransactionService | + +### Aturan Penamaan Service: +| Pattern | Keterangan | +|---------|------------| +| `private readonly XService $service` | Service property harus readonly | +| `= new XService` dilarang | Gunakan dependency injection, bukan default value | +| `getAll(array $filters = []): Collection` | Standar method listing | +| `paginated(int $perPage, string $search, string $sort, string $direction, array $filters): LengthAwarePaginator` | Standar method pagination | + +### Service Method Signatures: +```php +// Standard - dengan filter +getAll(array $filters = []): Collection +paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator + +// Standard - tanpa filter (tetap terima $filters untuk konsistensi) +getAll(): Collection +paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator +``` + +### Cash Transaction Patterns: +- **Credit (DEPOSIT)**: `$this->creditCash(amount, description)` → tambah saldo +- **Debit (EXPENSE/WITHDRAWAL)**: `$this->debitCash(amount, description)` → kurangi saldo + validasi + +### Stock Adjustment Patterns: +- **ProductVariant**: `$this->applyStock(items, stockType, sign)` → increment/decrement stock/reject_stock +- **RawMaterialPrice**: `$this->adjustStock(model, field, quantity, sign)` → increment/decrement + +### Service Files Diubah di Sesi Ini: +- `app/Services/Admin/Finance/CashAccountService.php` - hapus `= new`, tambah HandlesCashTransactions +- `app/Services/Admin/Finance/ExpenseService.php` - hapus `= new`, tambah HandlesCashTransactions +- `app/Services/Admin/Finance/EmployeeAdvanceService.php` - tambah HandlesCashTransactions +- `app/Services/Admin/Finance/PayrollPeriodService.php` - tambah HandlesCashTransactions +- `app/Services/Admin/Manage/TransactionService.php` - hapus `= new`, tambah HasStockAdjustment +- `app/Services/Admin/Manage/RestockService.php` - hapus `= new`, tambah HasStockAdjustment +- `app/Services/Admin/Manage/CuttingService.php` - hapus `= new`, hapus syncCuttingPhoto +- `app/Services/Admin/Manage/PurchaseService.php` - hapus `= new`, hapus syncPurchasePhoto +- `app/Services/Admin/Master/Product/ProductService.php` - hapus `= new` +- `app/Services/Admin/Master/Product/ProductVariantService.php` - hapus `= new` +- `app/Services/Admin/Master/RawMaterial/RawMaterialService.php` - hapus `= new` +- `app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php` - hapus `= new` +- `app/Services/Admin/AdminSettingsService.php` - hapus `= new` +- `app/Services/Admin/Settings/RoleService.php` - tambah $filters ke paginated() +- `app/Services/Admin/Master/CategoryService.php` - tambah $filters +- `app/Services/Admin/Master/CustomerService.php` - tambah $filters +- `app/Services/Admin/Master/SupplierService.php` - tambah $filters + +### Trait Files Baru: +- `app/Services/Concerns/HandlesCashTransactions.php` +- `app/Services/Concerns/HasStockAdjustment.php` + +--- + +## 13. Form Request Conventions - Perubahan di Sesi Ini + +### Traits (Shared Concerns) + +| Trait | Methods | Digunakan Oleh | +|-------|---------|----------------| +| `CurrencyStripping` | `stripCurrencyDot(array $data, string ...$fields): array` | 11 Form Requests | + +### Aturan Form Request: +| Pattern | Keterangan | +|---------|------------| +| `authorize()` wajib ada | Selalu return `true` (authorization di controller/middleware) | +| `attributes()` dalam Bahasa Indonesia | Label untuk semua field | +| `prepareForValidation()` | Strip dot currency via `CurrencyStripping` trait | +| Shared Store/Update | Gunakan 1 request dengan `sometimes` + if/else | +| Unique ignore | `Rule::unique('table')->ignore($this->route('model')?->id)` | + +### Currency Stripping Pattern: +```php +use App\Concerns\CurrencyStripping; + +class SomeRequest extends FormRequest +{ + use CurrencyStripping; + + public function prepareForValidation(): void + { + $this->merge($this->stripCurrencyDot($this->validated(), 'amount', 'discount', 'variants.*.price')); + } +} +``` + +### Form Request Files Diubah di Sesi Ini: + +#### Tambah `authorize()`: +- `app/Http/Requests/Settings/ProfileUpdateRequest.php` +- `app/Http/Requests/Settings/ProfileDeleteRequest.php` +- `app/Http/Requests/Settings/TwoFactorAuthenticationRequest.php` +- `app/Http/Requests/Settings/PasswordUpdateRequest.php` + +#### Tambah `CurrencyStripping` trait: +- `app/Http/Requests/Admin/Manage/TransactionRequest.php` - `discount`, `nego_price` +- `app/Http/Requests/Admin/Manage/PurchaseRequest.php` - `variants.*.price`, `discount`, `shipping_cost` +- `app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php` - `variants.*.price` +- `app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php` - `price` +- `app/Http/Requests/Admin/Master/Product/ProductRequest.php` - `shared_prices.*.price`, `variants.*.prices.*.price` +- `app/Http/Requests/Admin/Master/Product/ProductVariantRequest.php` - `prices.*.price` +- `app/Http/Requests/Admin/Finance/CashTransactionRequest.php` - `amount` +- `app/Http/Requests/Admin/Finance/ExpenseRequest.php` - `amount` +- `app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php` - `amount` +- `app/Http/Requests/Admin/Finance/PayrollAdjustmentRequest.php` - `amount` +- `app/Http/Requests/Admin/Settings/UpdateHRRequest.php` - `late_penalty_amount`, `absent_penalty_amount` + +### Trait Files Baru: +- `app/Concerns/CurrencyStripping.php` + +### Catatan Penting: +- **Shared Store/Update** sudah cukup pakai `sometimes` + if/else untuk field yang berbeda +- **Split hanya jika** ruleset store dan update sangat berbeda (jarang terjadi) +- **Currency stripping** selalu di `prepareForValidation()`, sebelum validasi jalan + +--- + +## 14. View Conventions - Perubahan di Sesi Ini + +### Tech Stack +- **Framework**: Inertia.js + React + TypeScript +- **UI Components**: shadcn/ui (customized) +- **Styling**: Tailwind CSS +- **Routing**: Ziggy (type-safe routes) + +### Directory Structure +``` +resources/js/ +├── pages/ # Page components (Inertia) +├── components/ # Shared UI components +├── hooks/ # Custom React hooks +├── lib/ # Utility functions +└── routes/ # Type-safe route definitions +``` + +### Page Patterns + +#### Simple CRUD (Category, Customer, Supplier) +```tsx +// State: createOpen, editing, deleting +// Components: PageHeader, FormDialog, DataTable, DeleteConfirmDialog +// Hook: useServerTable untuk pagination/search +``` + +#### Complex List (Transaction, Restock, Purchase, Cutting) +```tsx +// Components: PageHeader, CardTable (bukan DataTable) +// Features: FilterPopover, expandable rows, card + sub-row +// Hook: useServerTable + useCardTableExpand +``` + +### Columns Pattern +```tsx +// Type definition untuk entity +export type Category = { id: number; name: string; }; + +// Factory function untuk columns +export function createCategoryColumns(params: CreateColumnsParams): ColumnDef[] { + return [ + { accessorKey: 'name', header: ..., cell: ... }, + { id: 'actions', cell: ... RowActions ... } + ]; +} +``` + +### Enum Options dari Controller +```php +// Controller: kirim enum options ke view (pakai toSelect() dari HasValues trait) +'filterOptions' => [ + 'statusOptions' => OrderStatus::toSelect(), +], +``` + +```tsx +// View: gunakan options dari controller +{filterOptions.statusOptions.map((opt) => ( + + {opt.label} + +))} +``` + +### Draft Pattern +```tsx +// Hook: useXxxDraftSave (generated via createDraftHook) +export const useTransactionDraftSave = createDraftHook({ + save: saveTransactionDraft, + clear: clearTransactionDraft, +}); + +// Usage +useTransactionDraftSave('create', draftData, userId); +``` + +### Columns Pattern - Model Accessors +```tsx +// ❌ Jangan format di TypeScript +import { formatCurrency } from '@/lib/utils'; +{ + accessorKey: 'amount', + cell: ({ row }) => {formatCurrency(row.getValue('amount') as number)} +} + +// ✅ Gunakan formatted_ accessor dari model +{ + accessorKey: 'formatted_amount', + cell: ({ row }) => {row.getValue('formatted_amount') as string} +} + +// ✅ Tetap format di TS untuk computed values (bukan dari DB) +{ + accessorKey: 'payrolls_sum_total_amount', + cell: ({ row }) => {formatCurrency(row.getValue('payrolls_sum_total_amount') as number)} +} + +// ✅ Tetap format di TS untuk nested objects (bukan model attribute) +{ + cell: ({ row }) => {formatCurrency(row.original.adjustment.amount)} +} +``` + +### Format Functions +```tsx +import { formatDate, formatShortDate, formatDateTime } from '@/lib/format'; +import { formatCurrency, formatNumber } from '@/lib/utils'; + +formatDate(dateString) // "23 Agustus 2026 08:40" +formatShortDate(dateString) // "23 Agust 2026" +formatDateTime(dateString) // "23 Agustus 2026 08:40" +formatCurrency(1000000) // "Rp 1.000.000" +formatNumber(1000000) // "1.000.000" +``` + +### Shared Components +| Component | Digunakan Untuk | +|-----------|----------------| +| `FormDialog` | Modal form (create/edit) untuk simple CRUD | +| `DataTable` | Table dengan server-side pagination | +| `CardTable` | Card-based list dengan expandable rows | +| `PageHeader` | Header halaman dengan title + actions | +| `DeleteConfirmDialog` | Konfirmasi hapus | +| `FilterPopover` | Filter toolbar | +| `RowActions` | Action dropdown (edit/hapus) | +| `FileUpload` | Upload file ke S3 | +| `RupiahInput` | Input currency formatting | +| `NumberInput` | Input angka | + +### Hooks +| Hook | Fungsi | +|------|--------| +| `useServerTable` | Pagination, search, filter server-side | +| `useCardTableExpand` | Expand/collapse rows | +| `useXxxDraft` | Auto-save draft (transaction, product, restock, purchase, cutting, raw-material) | + +### Catatan Penting: +- **Enum options** harus dikirim dari controller, bukan hardcoded di view +- **Format data** gunakan `#[Appends]` attributes dari model +- **Type definitions** ada di `columns.tsx` bersama column definitions +- **Draft hooks** menggunakan `createDraftHook` factory \ No newline at end of file diff --git a/app/Enums/Concerns/HasValues.php b/app/Enums/Concerns/HasValues.php index 74452fc..effedcc 100644 --- a/app/Enums/Concerns/HasValues.php +++ b/app/Enums/Concerns/HasValues.php @@ -2,10 +2,23 @@ namespace App\Enums\Concerns; +use Illuminate\Support\Collection; + trait HasValues { public static function values(): array { return array_column(self::cases(), 'value'); } + + /** + * @return Collection + */ + public static function toSelect(): Collection + { + return collect(self::cases())->map(fn ($case) => [ + 'value' => $case->value, + 'label' => $case->label(), + ])->values(); + } } diff --git a/app/Http/Controllers/Admin/Finance/CashAccountController.php b/app/Http/Controllers/Admin/Finance/CashAccountController.php index 2a2b1f9..9fcf7e1 100644 --- a/app/Http/Controllers/Admin/Finance/CashAccountController.php +++ b/app/Http/Controllers/Admin/Finance/CashAccountController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin\Finance; +use App\Enums\CashTransactionType; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Finance\CashTransactionRequest; use App\Http\Requests\PaginatedRequest; @@ -28,6 +29,9 @@ public function index(PaginatedRequest $request): Response filters: $request->only(['type']), ), 'filters' => $request->only(['type']), + 'filterOptions' => [ + 'typeOptions' => CashTransactionType::toSelect(), + ], ]); } diff --git a/app/Http/Controllers/Admin/HR/LeaveRequestController.php b/app/Http/Controllers/Admin/HR/LeaveRequestController.php index 64bf1b7..22e6717 100644 --- a/app/Http/Controllers/Admin/HR/LeaveRequestController.php +++ b/app/Http/Controllers/Admin/HR/LeaveRequestController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin\HR; +use App\Enums\LeaveRequestStatus; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\HR\LeaveRequestRequest; use App\Http\Requests\PaginatedRequest; @@ -25,6 +26,9 @@ public function index(PaginatedRequest $request): Response filters: $request->only(['status']), ), 'filters' => $request->only(['status']), + 'filterOptions' => [ + 'statusOptions' => LeaveRequestStatus::toSelect(), + ], ]); } diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 0e7b9c5..d2ee439 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -18,7 +18,7 @@ use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; -#[Appends(['formatted_amount', 'type_label'])] +#[Appends(['formatted_amount', 'formatted_balance_after', 'formatted_created_at', 'type_label'])] #[Guarded(['id'])] class CashTransaction extends Model implements HasMedia { @@ -40,6 +40,20 @@ protected function formattedAmount(): Attribute ); } + protected function formattedBalanceAfter(): Attribute + { + return Attribute::make( + get: fn () => 'Rp ' . number_format($this->balance_after, 0, ',', '.'), + ); + } + + protected function formattedCreatedAt(): Attribute + { + return Attribute::make( + get: fn () => $this->created_at?->translatedFormat('l, d F Y'), + ); + } + protected function typeLabel(): Attribute { return Attribute::make( diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 3125adf..5c46789 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; -#[Appends(['formatted_amount', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])] +#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])] #[Guarded(['id'])] class EmployeeAdvance extends Model { @@ -38,6 +38,13 @@ protected function formattedAmount(): Attribute ); } + protected function formattedCreatedAt(): Attribute + { + return Attribute::make( + get: fn () => $this->created_at?->translatedFormat('l, d F Y'), + ); + } + protected function formattedDueDate(): Attribute { return Attribute::make( diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 47dd5b7..22dd3e2 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -12,7 +12,7 @@ use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; -#[Appends(['formatted_amount', 'formatted_date'])] +#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_date'])] #[Guarded(['id'])] class Expense extends Model implements HasMedia { @@ -33,6 +33,13 @@ protected function formattedAmount(): Attribute ); } + protected function formattedCreatedAt(): Attribute + { + return Attribute::make( + get: fn () => $this->created_at?->translatedFormat('l, d F Y'), + ); + } + protected function formattedDate(): Attribute { return Attribute::make( diff --git a/app/Services/Admin/HR/EmployeeService.php b/app/Services/Admin/HR/EmployeeService.php index a19e996..5d3887e 100644 --- a/app/Services/Admin/HR/EmployeeService.php +++ b/app/Services/Admin/HR/EmployeeService.php @@ -16,6 +16,7 @@ public function getAll(array $filters = []): Collection ->with([ 'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'), 'employee' => fn ($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'), + 'roles' => fn ($q) => $q->select('id', 'name'), ]) ->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status))) ->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN))) @@ -32,6 +33,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->with([ 'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'), 'employee' => fn ($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'), + 'roles' => fn ($q) => $q->select('id', 'name'), ]) ->when($search, fn ($q) => $q->whereHas('userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%"))) ->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status))) @@ -56,6 +58,8 @@ public function create(array $data): User 'is_active' => true, ]); + $user->assignRole($data['role']); + $user->userProfile()->create([ 'full_name' => $data['full_name'], 'phone_number' => $data['phone_number'] ?? null, @@ -64,12 +68,14 @@ public function create(array $data): User 'address' => $data['address'] ?? null, ]); - $user->employee()->create([ - 'join_date' => $data['join_date'], - 'resign_date' => $data['resign_date'] ?? null, - 'employment_status' => $data['employment_status'], - 'base_salary' => $data['base_salary'], - ]); + if ($data['role'] !== 'owner') { + $user->employee()->create([ + 'join_date' => $data['join_date'], + 'resign_date' => $data['resign_date'] ?? null, + 'employment_status' => $data['employment_status'], + 'base_salary' => $data['base_salary'], + ]); + } return $user; }); @@ -83,6 +89,8 @@ public function update(User $user, array $data): User 'username' => $data['username'], ]); + $user->syncRoles($data['role']); + $user->userProfile()->updateOrCreate([], [ 'full_name' => $data['full_name'], 'phone_number' => $data['phone_number'] ?? null, @@ -91,12 +99,16 @@ public function update(User $user, array $data): User 'address' => $data['address'] ?? null, ]); - $user->employee()->updateOrCreate([], [ - 'join_date' => $data['join_date'], - 'resign_date' => $data['resign_date'] ?? null, - 'employment_status' => $data['employment_status'], - 'base_salary' => $data['base_salary'], - ]); + if ($data['role'] !== 'owner') { + $user->employee()->updateOrCreate([], [ + 'join_date' => $data['join_date'], + 'resign_date' => $data['resign_date'] ?? null, + 'employment_status' => $data['employment_status'], + 'base_salary' => $data['base_salary'], + ]); + } else { + $user->employee()->delete(); + } }); return $user->fresh(['userProfile', 'employee']); diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index 8a93083..656ccc1 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -92,6 +92,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = public function getFilterOptions(): array { return [ + 'statusOptions' => OrderStatus::toSelect(), + 'channelOptions' => OrderChannel::toSelect(), + 'paymentTypeOptions' => PaymentType::toSelect(), 'customers' => Customer::query() ->select('id', 'name') ->orderBy('name') @@ -145,9 +148,9 @@ public function getForCreate(): array ->get() ->filter(fn (User $user) => $user->userProfile?->full_name) ->values(), - 'channelOptions' => collect(OrderChannel::cases())->map(fn ($c) => ['value' => $c->value, 'label' => $c->label()])->values(), - 'paymentTypeOptions' => collect(PaymentType::cases())->map(fn ($p) => ['value' => $p->value, 'label' => $p->label()])->values(), - 'priceTypeOptions' => collect(PriceType::cases())->filter(fn ($p) => ! in_array($p, [PriceType::CAPITAL]))->map(fn ($p) => ['value' => $p->value, 'label' => $p->label()])->values(), + 'channelOptions' => OrderChannel::toSelect(), + 'paymentTypeOptions' => PaymentType::toSelect(), + 'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(), ]; } diff --git a/resources/js/hooks/use-cutting-draft.ts b/resources/js/hooks/use-cutting-draft.ts index 6615c66..b98d6d9 100644 --- a/resources/js/hooks/use-cutting-draft.ts +++ b/resources/js/hooks/use-cutting-draft.ts @@ -1,20 +1,8 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; -import { clearCuttingDraft, saveCuttingDraft } from '@/lib/cutting-draft'; +import { createDraftHook } from '@/hooks/use-draft-save'; import type { CuttingDraftData } from '@/lib/cutting-draft'; +import { clearCuttingDraft, saveCuttingDraft } from '@/lib/cutting-draft'; -type DraftType = 'create' | 'edit'; - -export function useCuttingDraftSave( - type: DraftType, - data: CuttingDraftData, - userId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - delay, - store: { save: saveCuttingDraft, clear: clearCuttingDraft }, - }); -} +export const useCuttingDraftSave = createDraftHook({ + save: saveCuttingDraft, + clear: clearCuttingDraft, +}); diff --git a/resources/js/hooks/use-draft-save.ts b/resources/js/hooks/use-draft-save.ts index bf782d3..990384b 100644 --- a/resources/js/hooks/use-draft-save.ts +++ b/resources/js/hooks/use-draft-save.ts @@ -74,3 +74,24 @@ export function useDraftSave({ }; }, [type, userId, extraId, store]); } + +export function createDraftHook( + store: Pick, 'save' | 'load' | 'clear'>, +) { + return function useDraft( + type: DraftType, + data: D, + userId?: number, + extraId?: number, + delay = 500, + ) { + return useDraftSave({ + type, + data, + userId, + extraId, + delay, + store, + }); + }; +} diff --git a/resources/js/hooks/use-product-draft.ts b/resources/js/hooks/use-product-draft.ts index f8ca12c..4892a6c 100644 --- a/resources/js/hooks/use-product-draft.ts +++ b/resources/js/hooks/use-product-draft.ts @@ -1,22 +1,8 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; +import { createDraftHook } from '@/hooks/use-draft-save'; import { clearProductDraft, saveProductDraft } from '@/lib/product-draft'; import type { ProductDraftData } from '@/lib/product-draft'; -type DraftType = 'create' | 'edit'; - -export function useProductDraftSave( - type: DraftType, - data: ProductDraftData, - userId?: number, - productId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - extraId: productId, - delay, - store: { save: saveProductDraft, clear: clearProductDraft }, - }); -} +export const useProductDraftSave = createDraftHook({ + save: saveProductDraft, + clear: clearProductDraft, +}); diff --git a/resources/js/hooks/use-purchase-draft.ts b/resources/js/hooks/use-purchase-draft.ts index aba1306..d8780c7 100644 --- a/resources/js/hooks/use-purchase-draft.ts +++ b/resources/js/hooks/use-purchase-draft.ts @@ -1,20 +1,8 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; +import { createDraftHook } from '@/hooks/use-draft-save'; import { clearPurchaseDraft, savePurchaseDraft } from '@/lib/purchase-draft'; import type { PurchaseDraftData } from '@/lib/purchase-draft'; -type DraftType = 'create' | 'edit'; - -export function usePurchaseDraftSave( - type: DraftType, - data: PurchaseDraftData, - userId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - delay, - store: { save: savePurchaseDraft, clear: clearPurchaseDraft }, - }); -} +export const usePurchaseDraftSave = createDraftHook({ + save: savePurchaseDraft, + clear: clearPurchaseDraft, +}); diff --git a/resources/js/hooks/use-raw-material-draft.ts b/resources/js/hooks/use-raw-material-draft.ts index 50874f1..69e8aff 100644 --- a/resources/js/hooks/use-raw-material-draft.ts +++ b/resources/js/hooks/use-raw-material-draft.ts @@ -1,25 +1,11 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; +import { createDraftHook } from '@/hooks/use-draft-save'; import { clearRawMaterialDraft, saveRawMaterialDraft, } from '@/lib/raw-material-draft'; import type { RawMaterialDraftData } from '@/lib/raw-material-draft'; -type DraftType = 'create' | 'edit'; - -export function useRawMaterialDraftSave( - type: DraftType, - data: RawMaterialDraftData, - userId?: number, - rawMaterialId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - extraId: rawMaterialId, - delay, - store: { save: saveRawMaterialDraft, clear: clearRawMaterialDraft }, - }); -} +export const useRawMaterialDraftSave = createDraftHook({ + save: saveRawMaterialDraft, + clear: clearRawMaterialDraft, +}); diff --git a/resources/js/hooks/use-restock-draft.ts b/resources/js/hooks/use-restock-draft.ts index 94e4c6c..042881c 100644 --- a/resources/js/hooks/use-restock-draft.ts +++ b/resources/js/hooks/use-restock-draft.ts @@ -1,20 +1,8 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; +import { createDraftHook } from '@/hooks/use-draft-save'; import { clearRestockDraft, saveRestockDraft } from '@/lib/restock-draft'; import type { RestockDraftData } from '@/lib/restock-draft'; -type DraftType = 'create' | 'edit'; - -export function useRestockDraftSave( - type: DraftType, - data: RestockDraftData, - userId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - delay, - store: { save: saveRestockDraft, clear: clearRestockDraft }, - }); -} +export const useRestockDraftSave = createDraftHook({ + save: saveRestockDraft, + clear: clearRestockDraft, +}); diff --git a/resources/js/hooks/use-transaction-draft.ts b/resources/js/hooks/use-transaction-draft.ts index 0036409..b61cf23 100644 --- a/resources/js/hooks/use-transaction-draft.ts +++ b/resources/js/hooks/use-transaction-draft.ts @@ -1,23 +1,11 @@ -import { useDraftSave } from '@/hooks/use-draft-save'; +import { createDraftHook } from '@/hooks/use-draft-save'; import { clearTransactionDraft, saveTransactionDraft, } from '@/lib/transaction-draft'; import type { TransactionDraftData } from '@/lib/transaction-draft'; -type DraftType = 'create' | 'edit'; - -export function useTransactionDraftSave( - type: DraftType, - data: TransactionDraftData, - userId?: number, - delay = 500, -) { - return useDraftSave({ - type, - data, - userId, - delay, - store: { save: saveTransactionDraft, clear: clearTransactionDraft }, - }); -} +export const useTransactionDraftSave = createDraftHook({ + save: saveTransactionDraft, + clear: clearTransactionDraft, +}); diff --git a/resources/js/lib/format.ts b/resources/js/lib/format.ts index d244671..e10daf1 100644 --- a/resources/js/lib/format.ts +++ b/resources/js/lib/format.ts @@ -5,41 +5,33 @@ export function formatNumber( return new Intl.NumberFormat('id-ID', options).format(num); } -export function formatDate(dateString: string): string { +export type DateFormatStyle = 'full' | 'short' | 'datetime'; + +export function formatDate(dateString: string, style: DateFormatStyle = 'full'): string { const date = new Date(dateString); - return ( - date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }) + - ' ' + - date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - }) - ); -} - -export function formatShortDate(dateString: string): string { - const date = new Date(dateString); - - return date.toLocaleDateString('id-ID', { + const datePart = date.toLocaleDateString('id-ID', { day: '2-digit', - month: 'short', + month: style === 'short' ? 'short' : 'long', year: 'numeric', }); -} -export function formatDateTime(dateString: string): string { - const date = new Date(dateString); + if (style === 'short') { + return datePart; + } - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', + const timePart = date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', }); + + return `${datePart} ${timePart}`; +} + +export function formatShortDate(dateString: string): string { + return formatDate(dateString, 'short'); +} + +export function formatDateTime(dateString: string): string { + return formatDate(dateString, 'datetime'); } diff --git a/resources/js/pages/admin/finance/cash-account/columns.tsx b/resources/js/pages/admin/finance/cash-account/columns.tsx index d53d881..78d64af 100644 --- a/resources/js/pages/admin/finance/cash-account/columns.tsx +++ b/resources/js/pages/admin/finance/cash-account/columns.tsx @@ -1,12 +1,12 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; -import { formatCurrency } from '@/lib/utils'; export type CashAccount = { id: number; name: string; balance: number; + formatted_balance: string; }; type CreateColumnsParams = { @@ -30,11 +30,11 @@ export function createCashAccountColumns( ), }, { - accessorKey: 'balance', + accessorKey: 'formatted_balance', header: () => Saldo, cell: ({ row }) => ( - {formatCurrency(row.getValue('balance') as number)} + {row.getValue('formatted_balance') as string} ), }, diff --git a/resources/js/pages/admin/finance/cash-account/index.tsx b/resources/js/pages/admin/finance/cash-account/index.tsx index 19aaf03..0ad6c0f 100644 --- a/resources/js/pages/admin/finance/cash-account/index.tsx +++ b/resources/js/pages/admin/finance/cash-account/index.tsx @@ -41,6 +41,11 @@ type CashAccount = { balance: number; }; +type TypeOption = { + value: string; + label: string; +}; + type Props = { cashAccount: CashAccount | null; transactions: { @@ -53,12 +58,16 @@ type Props = { filters: { type?: string; }; + filterOptions: { + typeOptions: TypeOption[]; + }; }; export default function CashAccountIndex({ cashAccount, transactions, filters, + filterOptions, }: Props) { const [depositOpen, setDepositOpen] = useState(false); const [withdrawalOpen, setWithdrawalOpen] = useState(false); @@ -151,10 +160,11 @@ export default function CashAccountIndex({ Semua Tipe - Deposit - Withdrawal - Pengeluaran - Transfer + {filterOptions.typeOptions.map((opt) => ( + + {opt.label} + + ))} diff --git a/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx b/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx index 6c622ba..1609b86 100644 --- a/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx +++ b/resources/js/pages/admin/finance/cash-account/transaction-columns.tsx @@ -2,18 +2,19 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; import { ImagePreviewButton } from '@/components/image-preview-button'; import { RowActions } from '@/components/row-actions'; -import { formatDate } from '@/lib/format'; -import { formatCurrency } from '@/lib/utils'; export type CashTransaction = { id: number; amount: number; + formatted_amount: string; balance_after: number; + formatted_balance_after: string; type: 'deposit' | 'withdrawal' | 'expense' | 'transfer'; description: string; receipt_key: string | null; receipt_url: string | null; created_at: string; + formatted_created_at: string; created_by: { user_profile: { full_name: string; @@ -58,10 +59,10 @@ export function createTransactionColumns( return [ { - accessorKey: 'created_at', + accessorKey: 'formatted_created_at', header: () => Tanggal, cell: ({ row }) => ( - {formatDate(row.getValue('created_at') as string)} + {row.getValue('formatted_created_at') as string} ), }, { @@ -85,7 +86,7 @@ export function createTransactionColumns( }, }, { - accessorKey: 'amount', + accessorKey: 'formatted_amount', header: () => Jumlah, cell: ({ row }) => { const transaction = row.original; @@ -100,17 +101,17 @@ export function createTransactionColumns( } > {isDeposit ? '+' : '-'}{' '} - {formatCurrency(row.getValue('amount') as number)} + {row.getValue('formatted_amount') as string} ); }, }, { - accessorKey: 'balance_after', + accessorKey: 'formatted_balance_after', header: () => Saldo Setelah, cell: ({ row }) => ( - {formatCurrency(row.getValue('balance_after') as number)} + {row.getValue('formatted_balance_after') as string} ), }, diff --git a/resources/js/pages/admin/finance/employee-advance/columns.tsx b/resources/js/pages/admin/finance/employee-advance/columns.tsx index c4ccc57..def673f 100644 --- a/resources/js/pages/admin/finance/employee-advance/columns.tsx +++ b/resources/js/pages/admin/finance/employee-advance/columns.tsx @@ -2,17 +2,19 @@ import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; -import { formatDate, formatShortDate } from '@/lib/format'; -import { formatCurrency } from '@/lib/utils'; export type EmployeeAdvance = { id: number; amount: number; + formatted_amount: string; paid_amount: number; + formatted_paid_amount: string; description: string; due_date: string; + formatted_due_date: string; status: 'pending' | 'approved' | 'paid' | 'rejected' | 'cancelled'; created_at: string; + formatted_created_at: string; employee: { user: { user_profile: { @@ -69,10 +71,10 @@ export function createEmployeeAdvanceColumns( return [ { - accessorKey: 'created_at', + accessorKey: 'formatted_created_at', header: () => Tanggal, cell: ({ row }) => ( - {formatDate(row.getValue('created_at') as string)} + {row.getValue('formatted_created_at') as string} ), }, { @@ -89,11 +91,11 @@ export function createEmployeeAdvanceColumns( }, }, { - accessorKey: 'amount', + accessorKey: 'formatted_amount', header: () => Jumlah, cell: ({ row }) => ( - - {formatCurrency(row.getValue('amount') as number)} + - {row.getValue('formatted_amount') as string} ), }, @@ -107,11 +109,11 @@ export function createEmployeeAdvanceColumns( ), }, { - accessorKey: 'due_date', + accessorKey: 'formatted_due_date', header: () => Jatuh Tempo, cell: ({ row }) => ( - {formatShortDate(row.getValue('due_date') as string)} + {row.getValue('formatted_due_date') as string} ), }, diff --git a/resources/js/pages/admin/finance/expense/columns.tsx b/resources/js/pages/admin/finance/expense/columns.tsx index cfead1d..18f6c93 100644 --- a/resources/js/pages/admin/finance/expense/columns.tsx +++ b/resources/js/pages/admin/finance/expense/columns.tsx @@ -2,16 +2,16 @@ import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2 } from 'lucide-react'; import { ImagePreviewButton } from '@/components/image-preview-button'; import { RowActions } from '@/components/row-actions'; -import { formatDate } from '@/lib/format'; -import { formatCurrency } from '@/lib/utils'; export type Expense = { id: number; amount: number; + formatted_amount: string; description: string; receipt_key: string | null; receipt_url: string | null; created_at: string; + formatted_created_at: string; created_by: { user_profile: { full_name: string; @@ -31,10 +31,10 @@ export function createExpenseColumns( return [ { - accessorKey: 'created_at', + accessorKey: 'formatted_created_at', header: () => Tanggal, cell: ({ row }) => ( - {formatDate(row.getValue('created_at') as string)} + {row.getValue('formatted_created_at') as string} ), }, { @@ -65,11 +65,11 @@ export function createExpenseColumns( }, }, { - accessorKey: 'amount', + accessorKey: 'formatted_amount', header: () => Jumlah, cell: ({ row }) => ( - - {formatCurrency(row.getValue('amount') as number)} + - {row.getValue('formatted_amount') as string} ), }, diff --git a/resources/js/pages/admin/finance/payroll-period/show-columns.tsx b/resources/js/pages/admin/finance/payroll-period/show-columns.tsx index c07ab34..a4f9620 100644 --- a/resources/js/pages/admin/finance/payroll-period/show-columns.tsx +++ b/resources/js/pages/admin/finance/payroll-period/show-columns.tsx @@ -7,9 +7,13 @@ import { formatCurrency } from '@/lib/utils'; export type Payroll = { id: number; base_salary: number; + formatted_base_salary: string; bonus_amount: number; + formatted_bonus_amount: string; deduction_amount: number; + formatted_deduction_amount: string; total_amount: number; + formatted_total_amount: string; status: 'unpaid' | 'paid' | 'cancelled'; paid_at: string | null; employee: { @@ -90,19 +94,19 @@ export function createPayrollColumns( }, }, { - accessorKey: 'base_salary', + accessorKey: 'formatted_base_salary', header: () => Gaji Pokok, cell: ({ row }) => ( - {formatCurrency(row.getValue('base_salary') as number)} + {row.getValue('formatted_base_salary') as string} ), }, { - accessorKey: 'bonus_amount', + accessorKey: 'formatted_bonus_amount', header: () => Bonus, cell: ({ row }) => { - const value = row.getValue('bonus_amount') as number; + const value = row.original.bonus_amount; return ( {value > 0 ? '+ ' : ''} - {formatCurrency(value)} + {row.getValue('formatted_bonus_amount') as string} ); }, }, { - accessorKey: 'deduction_amount', + accessorKey: 'formatted_deduction_amount', header: () => Potongan, cell: ({ row }) => { - const value = row.getValue('deduction_amount') as number; + const value = row.original.deduction_amount; return ( 0 ? 'font-medium text-red-600' : ''} > {value > 0 ? '- ' : ''} - {formatCurrency(value)} + {row.getValue('formatted_deduction_amount') as string} ); }, }, { - accessorKey: 'total_amount', + accessorKey: 'formatted_total_amount', header: () => Total, cell: ({ row }) => ( - {formatCurrency(row.getValue('total_amount') as number)} + {row.getValue('formatted_total_amount') as string} ), }, diff --git a/resources/js/pages/admin/hr/employee/columns.tsx b/resources/js/pages/admin/hr/employee/columns.tsx index b6712a3..9aa9569 100644 --- a/resources/js/pages/admin/hr/employee/columns.tsx +++ b/resources/js/pages/admin/hr/employee/columns.tsx @@ -9,6 +9,7 @@ export type Employee = { email: string; username: string; is_active: boolean; + roles?: { name: string }[]; user_profile: { full_name: string; phone_number: string | null; @@ -90,6 +91,20 @@ export function createEmployeeColumns( ); }, }, + { + id: 'role', + header: () => Role, + cell: ({ row }) => { + const employee = row.original; + const roleName = employee.roles?.[0]?.name ?? '-'; + + return ( + + {roleName} + + ); + }, + }, { accessorKey: 'employee.employment_status', id: 'employment_status', diff --git a/resources/js/pages/admin/hr/leave-request/columns.tsx b/resources/js/pages/admin/hr/leave-request/columns.tsx index dd0826c..1bf3d3b 100644 --- a/resources/js/pages/admin/hr/leave-request/columns.tsx +++ b/resources/js/pages/admin/hr/leave-request/columns.tsx @@ -2,12 +2,13 @@ import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle, Pencil, Trash2, XCircle } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; -import { formatShortDate } from '@/lib/format'; export type LeaveRequest = { id: number; start_date: string; + formatted_start_date: string; end_date: string; + formatted_end_date: string; total_days: number; status: 'pending' | 'approved' | 'rejected' | 'cancelled'; created_at: string; @@ -77,20 +78,20 @@ export function createLeaveRequestColumns( }, }, { - accessorKey: 'start_date', + accessorKey: 'formatted_start_date', header: () => Tanggal Mulai, cell: ({ row }) => ( - {formatShortDate(row.getValue('start_date') as string)} + {row.getValue('formatted_start_date') as string} ), }, { - accessorKey: 'end_date', + accessorKey: 'formatted_end_date', header: () => Tanggal Selesai, cell: ({ row }) => ( - {formatShortDate(row.getValue('end_date') as string)} + {row.getValue('formatted_end_date') as string} ), }, diff --git a/resources/js/pages/admin/hr/leave-request/index.tsx b/resources/js/pages/admin/hr/leave-request/index.tsx index e3c8db8..4a75199 100644 --- a/resources/js/pages/admin/hr/leave-request/index.tsx +++ b/resources/js/pages/admin/hr/leave-request/index.tsx @@ -30,6 +30,11 @@ import { import type { LeaveRequest } from './columns'; import { createLeaveRequestColumns } from './columns'; +type StatusOption = { + value: string; + label: string; +}; + type Props = { leaveRequests: { data: LeaveRequest[]; @@ -41,9 +46,12 @@ type Props = { filters: { status?: string; }; + filterOptions: { + statusOptions: StatusOption[]; + }; }; -export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { +export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); @@ -154,10 +162,11 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) { Semua Status - Menunggu - Disetujui - Ditolak - Dibatalkan + {filterOptions.statusOptions.map((opt) => ( + + {opt.label} + + ))} diff --git a/resources/js/pages/admin/manage/transaction/index.tsx b/resources/js/pages/admin/manage/transaction/index.tsx index a7ef076..e871aff 100644 --- a/resources/js/pages/admin/manage/transaction/index.tsx +++ b/resources/js/pages/admin/manage/transaction/index.tsx @@ -38,6 +38,11 @@ type FilterOption = { name: string; }; +type StatusOption = { + value: string; + label: string; +}; + type Props = { transactions: { data: Transaction[]; @@ -55,6 +60,9 @@ type Props = { created_by_id?: string; }; filterOptions: { + statusOptions: StatusOption[]; + channelOptions: StatusOption[]; + paymentTypeOptions: StatusOption[]; customers: FilterOption[]; employees: FilterOption[]; }; @@ -150,11 +158,11 @@ export default function TransactionIndex({ Semua Status - Pending - Diproses - Selesai - Dibatalkan - Dikembalikan + {filterOptions.statusOptions.map((opt) => ( + + {opt.label} + + ))} @@ -171,9 +179,11 @@ export default function TransactionIndex({ Semua Channel - Toko - Shopee - TikTok + {filterOptions.channelOptions.map((opt) => ( + + {opt.label} + + ))} @@ -192,10 +202,11 @@ export default function TransactionIndex({ Semua Tipe - Tunai - Transfer - Marketplace - QRIS + {filterOptions.paymentTypeOptions.map((opt) => ( + + {opt.label} + + ))}