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.
This commit is contained in:
parent
81f8135273
commit
4517eeb7ee
957
AGENTS.md
Normal file
957
AGENTS.md
Normal file
@ -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<int, array{value: string, label: string}>`.
|
||||||
|
|
||||||
|
```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<Category>[] {
|
||||||
|
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) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Draft Pattern
|
||||||
|
```tsx
|
||||||
|
// Hook: useXxxDraftSave (generated via createDraftHook)
|
||||||
|
export const useTransactionDraftSave = createDraftHook<TransactionDraftData>({
|
||||||
|
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 }) => <span>{formatCurrency(row.getValue('amount') as number)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Gunakan formatted_ accessor dari model
|
||||||
|
{
|
||||||
|
accessorKey: 'formatted_amount',
|
||||||
|
cell: ({ row }) => <span>{row.getValue('formatted_amount') as string}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Tetap format di TS untuk computed values (bukan dari DB)
|
||||||
|
{
|
||||||
|
accessorKey: 'payrolls_sum_total_amount',
|
||||||
|
cell: ({ row }) => <span>{formatCurrency(row.getValue('payrolls_sum_total_amount') as number)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Tetap format di TS untuk nested objects (bukan model attribute)
|
||||||
|
{
|
||||||
|
cell: ({ row }) => <span>{formatCurrency(row.original.adjustment.amount)}</span>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
@ -2,10 +2,23 @@
|
|||||||
|
|
||||||
namespace App\Enums\Concerns;
|
namespace App\Enums\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
trait HasValues
|
trait HasValues
|
||||||
{
|
{
|
||||||
public static function values(): array
|
public static function values(): array
|
||||||
{
|
{
|
||||||
return array_column(self::cases(), 'value');
|
return array_column(self::cases(), 'value');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, array{value: string, label: string}>
|
||||||
|
*/
|
||||||
|
public static function toSelect(): Collection
|
||||||
|
{
|
||||||
|
return collect(self::cases())->map(fn ($case) => [
|
||||||
|
'value' => $case->value,
|
||||||
|
'label' => $case->label(),
|
||||||
|
])->values();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
@ -28,6 +29,9 @@ public function index(PaginatedRequest $request): Response
|
|||||||
filters: $request->only(['type']),
|
filters: $request->only(['type']),
|
||||||
),
|
),
|
||||||
'filters' => $request->only(['type']),
|
'filters' => $request->only(['type']),
|
||||||
|
'filterOptions' => [
|
||||||
|
'typeOptions' => CashTransactionType::toSelect(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\HR;
|
namespace App\Http\Controllers\Admin\HR;
|
||||||
|
|
||||||
|
use App\Enums\LeaveRequestStatus;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\HR\LeaveRequestRequest;
|
use App\Http\Requests\Admin\HR\LeaveRequestRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
@ -25,6 +26,9 @@ public function index(PaginatedRequest $request): Response
|
|||||||
filters: $request->only(['status']),
|
filters: $request->only(['status']),
|
||||||
),
|
),
|
||||||
'filters' => $request->only(['status']),
|
'filters' => $request->only(['status']),
|
||||||
|
'filterOptions' => [
|
||||||
|
'statusOptions' => LeaveRequestStatus::toSelect(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
#[Appends(['formatted_amount', 'type_label'])]
|
#[Appends(['formatted_amount', 'formatted_balance_after', 'formatted_created_at', 'type_label'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class CashTransaction extends Model implements HasMedia
|
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
|
protected function typeLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
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'])]
|
#[Guarded(['id'])]
|
||||||
class EmployeeAdvance extends Model
|
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
|
protected function formattedDueDate(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
#[Appends(['formatted_amount', 'formatted_date'])]
|
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_date'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class Expense extends Model implements HasMedia
|
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
|
protected function formattedDate(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
@ -16,6 +16,7 @@ public function getAll(array $filters = []): Collection
|
|||||||
->with([
|
->with([
|
||||||
'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
'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'),
|
'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($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)))
|
->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([
|
->with([
|
||||||
'userProfile' => fn ($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
'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'),
|
'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($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)))
|
->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,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$user->assignRole($data['role']);
|
||||||
|
|
||||||
$user->userProfile()->create([
|
$user->userProfile()->create([
|
||||||
'full_name' => $data['full_name'],
|
'full_name' => $data['full_name'],
|
||||||
'phone_number' => $data['phone_number'] ?? null,
|
'phone_number' => $data['phone_number'] ?? null,
|
||||||
@ -64,12 +68,14 @@ public function create(array $data): User
|
|||||||
'address' => $data['address'] ?? null,
|
'address' => $data['address'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->employee()->create([
|
if ($data['role'] !== 'owner') {
|
||||||
'join_date' => $data['join_date'],
|
$user->employee()->create([
|
||||||
'resign_date' => $data['resign_date'] ?? null,
|
'join_date' => $data['join_date'],
|
||||||
'employment_status' => $data['employment_status'],
|
'resign_date' => $data['resign_date'] ?? null,
|
||||||
'base_salary' => $data['base_salary'],
|
'employment_status' => $data['employment_status'],
|
||||||
]);
|
'base_salary' => $data['base_salary'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
});
|
});
|
||||||
@ -83,6 +89,8 @@ public function update(User $user, array $data): User
|
|||||||
'username' => $data['username'],
|
'username' => $data['username'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$user->syncRoles($data['role']);
|
||||||
|
|
||||||
$user->userProfile()->updateOrCreate([], [
|
$user->userProfile()->updateOrCreate([], [
|
||||||
'full_name' => $data['full_name'],
|
'full_name' => $data['full_name'],
|
||||||
'phone_number' => $data['phone_number'] ?? null,
|
'phone_number' => $data['phone_number'] ?? null,
|
||||||
@ -91,12 +99,16 @@ public function update(User $user, array $data): User
|
|||||||
'address' => $data['address'] ?? null,
|
'address' => $data['address'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user->employee()->updateOrCreate([], [
|
if ($data['role'] !== 'owner') {
|
||||||
'join_date' => $data['join_date'],
|
$user->employee()->updateOrCreate([], [
|
||||||
'resign_date' => $data['resign_date'] ?? null,
|
'join_date' => $data['join_date'],
|
||||||
'employment_status' => $data['employment_status'],
|
'resign_date' => $data['resign_date'] ?? null,
|
||||||
'base_salary' => $data['base_salary'],
|
'employment_status' => $data['employment_status'],
|
||||||
]);
|
'base_salary' => $data['base_salary'],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$user->employee()->delete();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return $user->fresh(['userProfile', 'employee']);
|
return $user->fresh(['userProfile', 'employee']);
|
||||||
|
|||||||
@ -92,6 +92,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
public function getFilterOptions(): array
|
public function getFilterOptions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'statusOptions' => OrderStatus::toSelect(),
|
||||||
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
'customers' => Customer::query()
|
'customers' => Customer::query()
|
||||||
->select('id', 'name')
|
->select('id', 'name')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
@ -145,9 +148,9 @@ public function getForCreate(): array
|
|||||||
->get()
|
->get()
|
||||||
->filter(fn (User $user) => $user->userProfile?->full_name)
|
->filter(fn (User $user) => $user->userProfile?->full_name)
|
||||||
->values(),
|
->values(),
|
||||||
'channelOptions' => collect(OrderChannel::cases())->map(fn ($c) => ['value' => $c->value, 'label' => $c->label()])->values(),
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
'paymentTypeOptions' => collect(PaymentType::cases())->map(fn ($p) => ['value' => $p->value, 'label' => $p->label()])->values(),
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
'priceTypeOptions' => collect(PriceType::cases())->filter(fn ($p) => ! in_array($p, [PriceType::CAPITAL]))->map(fn ($p) => ['value' => $p->value, 'label' => $p->label()])->values(),
|
'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,8 @@
|
|||||||
import { useDraftSave } from '@/hooks/use-draft-save';
|
import { createDraftHook } from '@/hooks/use-draft-save';
|
||||||
import { clearCuttingDraft, saveCuttingDraft } from '@/lib/cutting-draft';
|
|
||||||
import type { CuttingDraftData } from '@/lib/cutting-draft';
|
import type { CuttingDraftData } from '@/lib/cutting-draft';
|
||||||
|
import { clearCuttingDraft, saveCuttingDraft } from '@/lib/cutting-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const useCuttingDraftSave = createDraftHook<CuttingDraftData>({
|
||||||
|
save: saveCuttingDraft,
|
||||||
export function useCuttingDraftSave(
|
clear: clearCuttingDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: CuttingDraftData,
|
|
||||||
userId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
delay,
|
|
||||||
store: { save: saveCuttingDraft, clear: clearCuttingDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -74,3 +74,24 @@ export function useDraftSave<D>({
|
|||||||
};
|
};
|
||||||
}, [type, userId, extraId, store]);
|
}, [type, userId, extraId, store]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDraftHook<D>(
|
||||||
|
store: Pick<DraftStore<D>, 'save' | 'load' | 'clear'>,
|
||||||
|
) {
|
||||||
|
return function useDraft(
|
||||||
|
type: DraftType,
|
||||||
|
data: D,
|
||||||
|
userId?: number,
|
||||||
|
extraId?: number,
|
||||||
|
delay = 500,
|
||||||
|
) {
|
||||||
|
return useDraftSave({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
userId,
|
||||||
|
extraId,
|
||||||
|
delay,
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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 { clearProductDraft, saveProductDraft } from '@/lib/product-draft';
|
||||||
import type { ProductDraftData } from '@/lib/product-draft';
|
import type { ProductDraftData } from '@/lib/product-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const useProductDraftSave = createDraftHook<ProductDraftData>({
|
||||||
|
save: saveProductDraft,
|
||||||
export function useProductDraftSave(
|
clear: clearProductDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: ProductDraftData,
|
|
||||||
userId?: number,
|
|
||||||
productId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
extraId: productId,
|
|
||||||
delay,
|
|
||||||
store: { save: saveProductDraft, clear: clearProductDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 { clearPurchaseDraft, savePurchaseDraft } from '@/lib/purchase-draft';
|
||||||
import type { PurchaseDraftData } from '@/lib/purchase-draft';
|
import type { PurchaseDraftData } from '@/lib/purchase-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const usePurchaseDraftSave = createDraftHook<PurchaseDraftData>({
|
||||||
|
save: savePurchaseDraft,
|
||||||
export function usePurchaseDraftSave(
|
clear: clearPurchaseDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: PurchaseDraftData,
|
|
||||||
userId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
delay,
|
|
||||||
store: { save: savePurchaseDraft, clear: clearPurchaseDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,25 +1,11 @@
|
|||||||
import { useDraftSave } from '@/hooks/use-draft-save';
|
import { createDraftHook } from '@/hooks/use-draft-save';
|
||||||
import {
|
import {
|
||||||
clearRawMaterialDraft,
|
clearRawMaterialDraft,
|
||||||
saveRawMaterialDraft,
|
saveRawMaterialDraft,
|
||||||
} from '@/lib/raw-material-draft';
|
} from '@/lib/raw-material-draft';
|
||||||
import type { RawMaterialDraftData } from '@/lib/raw-material-draft';
|
import type { RawMaterialDraftData } from '@/lib/raw-material-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const useRawMaterialDraftSave = createDraftHook<RawMaterialDraftData>({
|
||||||
|
save: saveRawMaterialDraft,
|
||||||
export function useRawMaterialDraftSave(
|
clear: clearRawMaterialDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: RawMaterialDraftData,
|
|
||||||
userId?: number,
|
|
||||||
rawMaterialId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
extraId: rawMaterialId,
|
|
||||||
delay,
|
|
||||||
store: { save: saveRawMaterialDraft, clear: clearRawMaterialDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 { clearRestockDraft, saveRestockDraft } from '@/lib/restock-draft';
|
||||||
import type { RestockDraftData } from '@/lib/restock-draft';
|
import type { RestockDraftData } from '@/lib/restock-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const useRestockDraftSave = createDraftHook<RestockDraftData>({
|
||||||
|
save: saveRestockDraft,
|
||||||
export function useRestockDraftSave(
|
clear: clearRestockDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: RestockDraftData,
|
|
||||||
userId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
delay,
|
|
||||||
store: { save: saveRestockDraft, clear: clearRestockDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,23 +1,11 @@
|
|||||||
import { useDraftSave } from '@/hooks/use-draft-save';
|
import { createDraftHook } from '@/hooks/use-draft-save';
|
||||||
import {
|
import {
|
||||||
clearTransactionDraft,
|
clearTransactionDraft,
|
||||||
saveTransactionDraft,
|
saveTransactionDraft,
|
||||||
} from '@/lib/transaction-draft';
|
} from '@/lib/transaction-draft';
|
||||||
import type { TransactionDraftData } from '@/lib/transaction-draft';
|
import type { TransactionDraftData } from '@/lib/transaction-draft';
|
||||||
|
|
||||||
type DraftType = 'create' | 'edit';
|
export const useTransactionDraftSave = createDraftHook<TransactionDraftData>({
|
||||||
|
save: saveTransactionDraft,
|
||||||
export function useTransactionDraftSave(
|
clear: clearTransactionDraft,
|
||||||
type: DraftType,
|
});
|
||||||
data: TransactionDraftData,
|
|
||||||
userId?: number,
|
|
||||||
delay = 500,
|
|
||||||
) {
|
|
||||||
return useDraftSave({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
userId,
|
|
||||||
delay,
|
|
||||||
store: { save: saveTransactionDraft, clear: clearTransactionDraft },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,41 +5,33 @@ export function formatNumber(
|
|||||||
return new Intl.NumberFormat('id-ID', options).format(num);
|
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);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
return (
|
const datePart = date.toLocaleDateString('id-ID', {
|
||||||
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', {
|
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'short',
|
month: style === 'short' ? 'short' : 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDateTime(dateString: string): string {
|
if (style === 'short') {
|
||||||
const date = new Date(dateString);
|
return datePart;
|
||||||
|
}
|
||||||
|
|
||||||
return date.toLocaleDateString('id-ID', {
|
const timePart = date.toLocaleTimeString('id-ID', {
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type CashAccount = {
|
export type CashAccount = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
balance: number;
|
balance: number;
|
||||||
|
formatted_balance: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
@ -30,11 +30,11 @@ export function createCashAccountColumns(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'balance',
|
accessorKey: 'formatted_balance',
|
||||||
header: () => <span>Saldo</span>,
|
header: () => <span>Saldo</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{formatCurrency(row.getValue('balance') as number)}
|
{row.getValue('formatted_balance') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -41,6 +41,11 @@ type CashAccount = {
|
|||||||
balance: number;
|
balance: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TypeOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
cashAccount: CashAccount | null;
|
cashAccount: CashAccount | null;
|
||||||
transactions: {
|
transactions: {
|
||||||
@ -53,12 +58,16 @@ type Props = {
|
|||||||
filters: {
|
filters: {
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
|
filterOptions: {
|
||||||
|
typeOptions: TypeOption[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CashAccountIndex({
|
export default function CashAccountIndex({
|
||||||
cashAccount,
|
cashAccount,
|
||||||
transactions,
|
transactions,
|
||||||
filters,
|
filters,
|
||||||
|
filterOptions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [depositOpen, setDepositOpen] = useState(false);
|
const [depositOpen, setDepositOpen] = useState(false);
|
||||||
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
||||||
@ -151,10 +160,11 @@ export default function CashAccountIndex({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Tipe</SelectItem>
|
<SelectItem value="all">Semua Tipe</SelectItem>
|
||||||
<SelectItem value="deposit">Deposit</SelectItem>
|
{filterOptions.typeOptions.map((opt) => (
|
||||||
<SelectItem value="withdrawal">Withdrawal</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
<SelectItem value="expense">Pengeluaran</SelectItem>
|
{opt.label}
|
||||||
<SelectItem value="transfer">Transfer</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,18 +2,19 @@ import type { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
import { ImagePreviewButton } from '@/components/image-preview-button';
|
import { ImagePreviewButton } from '@/components/image-preview-button';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { formatDate } from '@/lib/format';
|
|
||||||
import { formatCurrency } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type CashTransaction = {
|
export type CashTransaction = {
|
||||||
id: number;
|
id: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
formatted_amount: string;
|
||||||
balance_after: number;
|
balance_after: number;
|
||||||
|
formatted_balance_after: string;
|
||||||
type: 'deposit' | 'withdrawal' | 'expense' | 'transfer';
|
type: 'deposit' | 'withdrawal' | 'expense' | 'transfer';
|
||||||
description: string;
|
description: string;
|
||||||
receipt_key: string | null;
|
receipt_key: string | null;
|
||||||
receipt_url: string | null;
|
receipt_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
formatted_created_at: string;
|
||||||
created_by: {
|
created_by: {
|
||||||
user_profile: {
|
user_profile: {
|
||||||
full_name: string;
|
full_name: string;
|
||||||
@ -58,10 +59,10 @@ export function createTransactionColumns(
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>{formatDate(row.getValue('created_at') as string)}</span>
|
<span>{row.getValue('formatted_created_at') as string}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -85,7 +86,7 @@ export function createTransactionColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const transaction = row.original;
|
const transaction = row.original;
|
||||||
@ -100,17 +101,17 @@ export function createTransactionColumns(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDeposit ? '+' : '-'}{' '}
|
{isDeposit ? '+' : '-'}{' '}
|
||||||
{formatCurrency(row.getValue('amount') as number)}
|
{row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'balance_after',
|
accessorKey: 'formatted_balance_after',
|
||||||
header: () => <span>Saldo Setelah</span>,
|
header: () => <span>Saldo Setelah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{formatCurrency(row.getValue('balance_after') as number)}
|
{row.getValue('formatted_balance_after') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import type { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react';
|
import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { formatDate, formatShortDate } from '@/lib/format';
|
|
||||||
import { formatCurrency } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type EmployeeAdvance = {
|
export type EmployeeAdvance = {
|
||||||
id: number;
|
id: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
formatted_amount: string;
|
||||||
paid_amount: number;
|
paid_amount: number;
|
||||||
|
formatted_paid_amount: string;
|
||||||
description: string;
|
description: string;
|
||||||
due_date: string;
|
due_date: string;
|
||||||
|
formatted_due_date: string;
|
||||||
status: 'pending' | 'approved' | 'paid' | 'rejected' | 'cancelled';
|
status: 'pending' | 'approved' | 'paid' | 'rejected' | 'cancelled';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
formatted_created_at: string;
|
||||||
employee: {
|
employee: {
|
||||||
user: {
|
user: {
|
||||||
user_profile: {
|
user_profile: {
|
||||||
@ -69,10 +71,10 @@ export function createEmployeeAdvanceColumns(
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>{formatDate(row.getValue('created_at') as string)}</span>
|
<span>{row.getValue('formatted_created_at') as string}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -89,11 +91,11 @@ export function createEmployeeAdvanceColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium text-red-600">
|
<span className="font-medium text-red-600">
|
||||||
- {formatCurrency(row.getValue('amount') as number)}
|
- {row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -107,11 +109,11 @@ export function createEmployeeAdvanceColumns(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'due_date',
|
accessorKey: 'formatted_due_date',
|
||||||
header: () => <span>Jatuh Tempo</span>,
|
header: () => <span>Jatuh Tempo</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>
|
||||||
{formatShortDate(row.getValue('due_date') as string)}
|
{row.getValue('formatted_due_date') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,16 +2,16 @@ import type { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
import { ImagePreviewButton } from '@/components/image-preview-button';
|
import { ImagePreviewButton } from '@/components/image-preview-button';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { formatDate } from '@/lib/format';
|
|
||||||
import { formatCurrency } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type Expense = {
|
export type Expense = {
|
||||||
id: number;
|
id: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
formatted_amount: string;
|
||||||
description: string;
|
description: string;
|
||||||
receipt_key: string | null;
|
receipt_key: string | null;
|
||||||
receipt_url: string | null;
|
receipt_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
formatted_created_at: string;
|
||||||
created_by: {
|
created_by: {
|
||||||
user_profile: {
|
user_profile: {
|
||||||
full_name: string;
|
full_name: string;
|
||||||
@ -31,10 +31,10 @@ export function createExpenseColumns(
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'formatted_created_at',
|
||||||
header: () => <span>Tanggal</span>,
|
header: () => <span>Tanggal</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>{formatDate(row.getValue('created_at') as string)}</span>
|
<span>{row.getValue('formatted_created_at') as string}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -65,11 +65,11 @@ export function createExpenseColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium text-red-600">
|
<span className="font-medium text-red-600">
|
||||||
- {formatCurrency(row.getValue('amount') as number)}
|
- {row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,9 +7,13 @@ import { formatCurrency } from '@/lib/utils';
|
|||||||
export type Payroll = {
|
export type Payroll = {
|
||||||
id: number;
|
id: number;
|
||||||
base_salary: number;
|
base_salary: number;
|
||||||
|
formatted_base_salary: string;
|
||||||
bonus_amount: number;
|
bonus_amount: number;
|
||||||
|
formatted_bonus_amount: string;
|
||||||
deduction_amount: number;
|
deduction_amount: number;
|
||||||
|
formatted_deduction_amount: string;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
|
formatted_total_amount: string;
|
||||||
status: 'unpaid' | 'paid' | 'cancelled';
|
status: 'unpaid' | 'paid' | 'cancelled';
|
||||||
paid_at: string | null;
|
paid_at: string | null;
|
||||||
employee: {
|
employee: {
|
||||||
@ -90,19 +94,19 @@ export function createPayrollColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'base_salary',
|
accessorKey: 'formatted_base_salary',
|
||||||
header: () => <span>Gaji Pokok</span>,
|
header: () => <span>Gaji Pokok</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>
|
||||||
{formatCurrency(row.getValue('base_salary') as number)}
|
{row.getValue('formatted_base_salary') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'bonus_amount',
|
accessorKey: 'formatted_bonus_amount',
|
||||||
header: () => <span>Bonus</span>,
|
header: () => <span>Bonus</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue('bonus_amount') as number;
|
const value = row.original.bonus_amount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -111,33 +115,33 @@ export function createPayrollColumns(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{value > 0 ? '+ ' : ''}
|
{value > 0 ? '+ ' : ''}
|
||||||
{formatCurrency(value)}
|
{row.getValue('formatted_bonus_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'deduction_amount',
|
accessorKey: 'formatted_deduction_amount',
|
||||||
header: () => <span>Potongan</span>,
|
header: () => <span>Potongan</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.getValue('deduction_amount') as number;
|
const value = row.original.deduction_amount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={value > 0 ? 'font-medium text-red-600' : ''}
|
className={value > 0 ? 'font-medium text-red-600' : ''}
|
||||||
>
|
>
|
||||||
{value > 0 ? '- ' : ''}
|
{value > 0 ? '- ' : ''}
|
||||||
{formatCurrency(value)}
|
{row.getValue('formatted_deduction_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'total_amount',
|
accessorKey: 'formatted_total_amount',
|
||||||
header: () => <span>Total</span>,
|
header: () => <span>Total</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{formatCurrency(row.getValue('total_amount') as number)}
|
{row.getValue('formatted_total_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type Employee = {
|
|||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
roles?: { name: string }[];
|
||||||
user_profile: {
|
user_profile: {
|
||||||
full_name: string;
|
full_name: string;
|
||||||
phone_number: string | null;
|
phone_number: string | null;
|
||||||
@ -90,6 +91,20 @@ export function createEmployeeColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
header: () => <span>Role</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const employee = row.original;
|
||||||
|
const roleName = employee.roles?.[0]?.name ?? '-';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
|
||||||
|
{roleName}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'employee.employment_status',
|
accessorKey: 'employee.employment_status',
|
||||||
id: 'employment_status',
|
id: 'employment_status',
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import type { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { CheckCircle, Pencil, Trash2, XCircle } from 'lucide-react';
|
import { CheckCircle, Pencil, Trash2, XCircle } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { formatShortDate } from '@/lib/format';
|
|
||||||
|
|
||||||
export type LeaveRequest = {
|
export type LeaveRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
start_date: string;
|
start_date: string;
|
||||||
|
formatted_start_date: string;
|
||||||
end_date: string;
|
end_date: string;
|
||||||
|
formatted_end_date: string;
|
||||||
total_days: number;
|
total_days: number;
|
||||||
status: 'pending' | 'approved' | 'rejected' | 'cancelled';
|
status: 'pending' | 'approved' | 'rejected' | 'cancelled';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@ -77,20 +78,20 @@ export function createLeaveRequestColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'start_date',
|
accessorKey: 'formatted_start_date',
|
||||||
header: () => <span>Tanggal Mulai</span>,
|
header: () => <span>Tanggal Mulai</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>
|
||||||
{formatShortDate(row.getValue('start_date') as string)}
|
{row.getValue('formatted_start_date') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'end_date',
|
accessorKey: 'formatted_end_date',
|
||||||
header: () => <span>Tanggal Selesai</span>,
|
header: () => <span>Tanggal Selesai</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span>
|
<span>
|
||||||
{formatShortDate(row.getValue('end_date') as string)}
|
{row.getValue('formatted_end_date') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -30,6 +30,11 @@ import {
|
|||||||
import type { LeaveRequest } from './columns';
|
import type { LeaveRequest } from './columns';
|
||||||
import { createLeaveRequestColumns } from './columns';
|
import { createLeaveRequestColumns } from './columns';
|
||||||
|
|
||||||
|
type StatusOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
leaveRequests: {
|
leaveRequests: {
|
||||||
data: LeaveRequest[];
|
data: LeaveRequest[];
|
||||||
@ -41,9 +46,12 @@ type Props = {
|
|||||||
filters: {
|
filters: {
|
||||||
status?: string;
|
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 [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
||||||
const [deleting, setDeleting] = useState<LeaveRequest | null>(null);
|
const [deleting, setDeleting] = useState<LeaveRequest | null>(null);
|
||||||
@ -154,10 +162,11 @@ export default function LeaveRequestIndex({ leaveRequests, filters }: Props) {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
<SelectItem value="all">Semua Status</SelectItem>
|
||||||
<SelectItem value="pending">Menunggu</SelectItem>
|
{filterOptions.statusOptions.map((opt) => (
|
||||||
<SelectItem value="approved">Disetujui</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
<SelectItem value="rejected">Ditolak</SelectItem>
|
{opt.label}
|
||||||
<SelectItem value="cancelled">Dibatalkan</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -38,6 +38,11 @@ type FilterOption = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StatusOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
transactions: {
|
transactions: {
|
||||||
data: Transaction[];
|
data: Transaction[];
|
||||||
@ -55,6 +60,9 @@ type Props = {
|
|||||||
created_by_id?: string;
|
created_by_id?: string;
|
||||||
};
|
};
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
|
statusOptions: StatusOption[];
|
||||||
|
channelOptions: StatusOption[];
|
||||||
|
paymentTypeOptions: StatusOption[];
|
||||||
customers: FilterOption[];
|
customers: FilterOption[];
|
||||||
employees: FilterOption[];
|
employees: FilterOption[];
|
||||||
};
|
};
|
||||||
@ -150,11 +158,11 @@ export default function TransactionIndex({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
<SelectItem value="all">Semua Status</SelectItem>
|
||||||
<SelectItem value="pending">Pending</SelectItem>
|
{filterOptions.statusOptions.map((opt) => (
|
||||||
<SelectItem value="processing">Diproses</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
<SelectItem value="completed">Selesai</SelectItem>
|
{opt.label}
|
||||||
<SelectItem value="cancelled">Dibatalkan</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="refunded">Dikembalikan</SelectItem>
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -171,9 +179,11 @@ export default function TransactionIndex({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Channel</SelectItem>
|
<SelectItem value="all">Semua Channel</SelectItem>
|
||||||
<SelectItem value="store">Toko</SelectItem>
|
{filterOptions.channelOptions.map((opt) => (
|
||||||
<SelectItem value="shopee">Shopee</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
<SelectItem value="tiktok">TikTok</SelectItem>
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -192,10 +202,11 @@ export default function TransactionIndex({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Tipe</SelectItem>
|
<SelectItem value="all">Semua Tipe</SelectItem>
|
||||||
<SelectItem value="cash">Tunai</SelectItem>
|
{filterOptions.paymentTypeOptions.map((opt) => (
|
||||||
<SelectItem value="transfer">Transfer</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
<SelectItem value="marketplace">Marketplace</SelectItem>
|
{opt.label}
|
||||||
<SelectItem value="qris">QRIS</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user