Compare commits

...

10 Commits

Author SHA1 Message Date
Yoga Pangestu
6546e1e3eb fix: update prepareForValidation to use all input data for currency stripping 2026-08-05 00:04:02 +07:00
Yoga Pangestu
3e9776696d Refactor permissions and sidebar items; enhance user role checks
- Updated HandleInertiaRequests middleware to include user permissions in the shared data.
- Modified RolePermissionSeeder to redefine permissions and roles, adding new permissions and restructuring existing ones.
- Refactored app-sidebar component to filter menu items based on user permissions, improving sidebar visibility based on roles.
- Enhanced useCan hook to support checking multiple permissions with canAny function.
- Updated web routes to enforce permission checks on various resource routes, ensuring proper access control.
2026-08-04 23:57:00 +07:00
Yoga Pangestu
0a688f1028 refactor: streamline user query methods for improved readability and maintainability 2026-08-04 23:15:00 +07:00
Yoga Pangestu
a8a7ed4c69 fix: handle potential null value for gender in genderLabel method 2026-08-04 23:08:39 +07:00
Yoga Pangestu
4517eeb7ee 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.
2026-08-04 23:03:56 +07:00
Yoga Pangestu
81f8135273 feat: add role selection to employee create and edit forms with conditional fields 2026-08-04 22:51:56 +07:00
Yoga Pangestu
318afca796 feat: add CurrencyStripping trait for consistent currency handling across requests 2026-08-04 22:11:50 +07:00
Yoga Pangestu
d90d2261ac feat: implement cash and stock handling traits for improved transaction management 2026-08-04 21:57:38 +07:00
Yoga Pangestu
8970c47a4a refactor: update constructors to use readonly properties for service dependencies 2026-08-04 15:58:11 +07:00
Yoga Pangestu
5961a60585 feat: add labels and formatting attributes to various models and enums
- Added labels for leave request statuses in LeaveRequestStatus enum.
- Introduced label methods for PayrollAdjustmentType, PayrollPeriodStatus, PayrollStatus, RawMaterialUnit, and StokOpnameStatus enums.
- Implemented formatted attributes for Attendance, CashAccount, CashTransaction, Category, Customer, Cutting, Employee, EmployeeAdvance, Expense, LeaveRequest, Order, OrderItem, Payroll, PayrollAdjustment, PayrollPeriod, Product, ProductPrice, ProductVariant, Purchase, PurchaseItem, RawMaterial, RawMaterialPrice, Restock, RestockItem, StokOpname, StokOpnameItem, Supplier, User, and UserProfile models.
- Enhanced phone number formatting for Customer and Supplier models.
- Added date formatting for various models to improve readability.
2026-08-04 15:31:41 +07:00
135 changed files with 3905 additions and 1140 deletions

957
AGENTS.md Normal file
View 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

View File

@ -0,0 +1,57 @@
<?php
namespace App\Concerns;
trait CurrencyStripping
{
protected function stripCurrencyDot(array $data, string ...$fields): array
{
foreach ($fields as $field) {
$data = $this->stripField($data, $field);
}
return $data;
}
private function stripField(array $data, string $field): array
{
if (str_contains($field, '*')) {
return $this->stripNestedField($data, $field);
}
if (isset($data[$field]) && is_string($data[$field])) {
$data[$field] = (int) str_replace('.', '', $data[$field]);
}
return $data;
}
private function stripNestedField(array $data, string $pattern): array
{
$parts = explode('.', $pattern);
$this->stripNestedRecursive($data, $parts);
return $data;
}
private function stripNestedRecursive(array &$data, array $parts): void
{
$current = array_shift($parts);
if ($current === '*') {
if (is_array($data)) {
foreach ($data as &$item) {
$this->stripNestedRecursive($item, $parts);
}
}
} elseif (empty($parts)) {
if (isset($data[$current]) && is_string($data[$current])) {
$data[$current] = (int) str_replace('.', '', $data[$current]);
}
} else {
if (isset($data[$current]) && is_array($data[$current])) {
$this->stripNestedRecursive($data[$current], $parts);
}
}
}
}

View File

@ -9,7 +9,17 @@ enum CashTransactionType: string
use HasValues; use HasValues;
case DEPOSIT = 'deposit'; case DEPOSIT = 'deposit';
case WITHDRAWAL = 'withdrawal';
case EXPENSE = 'expense'; case EXPENSE = 'expense';
case TRANSFER = 'transfer'; case TRANSFER = 'transfer';
case WITHDRAWAL = 'withdrawal';
public function label(): string
{
return match ($this) {
self::DEPOSIT => 'Setoran',
self::EXPENSE => 'Pengeluaran',
self::TRANSFER => 'Transfer',
self::WITHDRAWAL => 'Penarikan',
};
}
} }

View File

@ -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();
}
} }

View File

@ -8,7 +8,16 @@ enum CuttingStatus: string
{ {
use HasValues; use HasValues;
case IN_PROGRESS = 'in_progress';
case COMPLETED = 'completed';
case CANCELLED = 'cancelled'; case CANCELLED = 'cancelled';
case COMPLETED = 'completed';
case IN_PROGRESS = 'in_progress';
public function label(): string
{
return match ($this) {
self::CANCELLED => 'Dibatalkan',
self::COMPLETED => 'Selesai',
self::IN_PROGRESS => 'Dalam Proses',
};
}
} }

View File

@ -8,9 +8,20 @@ enum EmployeeAdvanceStatus: string
{ {
use HasValues; use HasValues;
case PENDING = 'pending';
case APPROVED = 'approved'; case APPROVED = 'approved';
case PAID = 'paid';
case REJECTED = 'rejected';
case CANCELLED = 'cancelled'; case CANCELLED = 'cancelled';
case PAID = 'paid';
case PENDING = 'pending';
case REJECTED = 'rejected';
public function label(): string
{
return match ($this) {
self::APPROVED => 'Disetujui',
self::CANCELLED => 'Dibatalkan',
self::PAID => 'Dibayar',
self::PENDING => 'Menunggu',
self::REJECTED => 'Ditolak',
};
}
} }

View File

@ -8,9 +8,20 @@ enum EmploymentStatus: string
{ {
use HasValues; use HasValues;
case FULL_TIME = 'full_time';
case PART_TIME = 'part_time';
case CONTRACT = 'contract'; case CONTRACT = 'contract';
case FULL_TIME = 'full_time';
case INTERNSHIP = 'internship'; case INTERNSHIP = 'internship';
case PART_TIME = 'part_time';
case RESIGNED = 'resigned'; case RESIGNED = 'resigned';
public function label(): string
{
return match ($this) {
self::CONTRACT => 'Kontrak',
self::FULL_TIME => 'Penuh Waktu',
self::INTERNSHIP => 'Magang',
self::PART_TIME => 'Paruh Waktu',
self::RESIGNED => 'Keluar',
};
}
} }

View File

@ -8,6 +8,14 @@ enum Gender: string
{ {
use HasValues; use HasValues;
case MALE = 'male';
case FEMALE = 'female'; case FEMALE = 'female';
case MALE = 'male';
public function label(): string
{
return match ($this) {
self::FEMALE => 'Perempuan',
self::MALE => 'Laki-laki',
};
}
} }

View File

@ -8,8 +8,18 @@ enum LeaveRequestStatus: string
{ {
use HasValues; use HasValues;
case PENDING = 'pending';
case APPROVED = 'approved'; case APPROVED = 'approved';
case REJECTED = 'rejected';
case CANCELLED = 'cancelled'; case CANCELLED = 'cancelled';
case PENDING = 'pending';
case REJECTED = 'rejected';
public function label(): string
{
return match ($this) {
self::APPROVED => 'Disetujui',
self::CANCELLED => 'Dibatalkan',
self::PENDING => 'Menunggu',
self::REJECTED => 'Ditolak',
};
}
} }

View File

@ -10,4 +10,12 @@ enum PayrollAdjustmentType: string
case BONUS = 'bonus'; case BONUS = 'bonus';
case DEDUCTION = 'deduction'; case DEDUCTION = 'deduction';
public function label(): string
{
return match ($this) {
self::BONUS => 'Bonus',
self::DEDUCTION => 'Potongan',
};
}
} }

View File

@ -8,6 +8,14 @@ enum PayrollPeriodStatus: string
{ {
use HasValues; use HasValues;
case OPEN = 'open';
case CLOSED = 'closed'; case CLOSED = 'closed';
case OPEN = 'open';
public function label(): string
{
return match ($this) {
self::CLOSED => 'Tutup',
self::OPEN => 'Buka',
};
}
} }

View File

@ -8,7 +8,16 @@ enum PayrollStatus: string
{ {
use HasValues; use HasValues;
case UNPAID = 'unpaid';
case PAID = 'paid';
case CANCELLED = 'cancelled'; case CANCELLED = 'cancelled';
case PAID = 'paid';
case UNPAID = 'unpaid';
public function label(): string
{
return match ($this) {
self::CANCELLED => 'Dibatalkan',
self::PAID => 'Dibayar',
self::UNPAID => 'Belum Dibayar',
};
}
} }

184
app/Enums/Permission.php Normal file
View File

@ -0,0 +1,184 @@
<?php
namespace App\Enums;
enum Permission: string
{
// Dashboard
case DASHBOARD_VIEW = 'dashboard.view';
case DASHBOARD_ATTENDANCE = 'dashboard.attendance';
case DASHBOARD_REVENUE = 'dashboard.revenue';
case DASHBOARD_EXPENSE = 'dashboard.expense';
case DASHBOARD_ORDERS_CHANNEL = 'dashboard.orders_channel';
case DASHBOARD_ORDERS_PAYMENT = 'dashboard.orders_payment';
case DASHBOARD_ORDERS_MARKETING = 'dashboard.orders_marketing';
case DASHBOARD_ORDERS_STATUS = 'dashboard.orders_status';
case DASHBOARD_CASH = 'dashboard.cash';
// Analysis
case ANALYSIS_VIEW = 'analysis.view';
case ANALYSIS_ATTENDANCE = 'analysis.attendance';
case ANALYSIS_CASH = 'analysis.cash';
case ANALYSIS_PRODUCT_STOCK = 'analysis.product_stock';
case ANALYSIS_REVENUE = 'analysis.revenue';
case ANALYSIS_EXPENSE = 'analysis.expense';
case ANALYSIS_BUSY_HOURS = 'analysis.busy_hours';
case ANALYSIS_PROFIT_ORDERS = 'analysis.profit_orders';
case ANALYSIS_PROFIT_HPP = 'analysis.profit_hpp';
case ANALYSIS_PROFIT_GROSS = 'analysis.profit_gross';
case ANALYSIS_PROFIT_MARGIN = 'analysis.profit_margin';
case ANALYSIS_TOP_CUSTOMERS = 'analysis.top_customers';
case ANALYSIS_TOP_PRODUCTS = 'analysis.top_products';
case ANALYSIS_MARKETING_SALES = 'analysis.marketing_sales';
case ANALYSIS_RAW_MATERIALS = 'analysis.raw_materials';
// Employees
case EMPLOYEES_VIEW = 'employees.view';
case EMPLOYEES_CREATE = 'employees.create';
case EMPLOYEES_UPDATE = 'employees.update';
case EMPLOYEES_DELETE = 'employees.delete';
case EMPLOYEES_TOGGLE_STATUS = 'employees.toggle_status';
case EMPLOYEES_RESET_PASSWORD = 'employees.reset_password';
// Attendances
case ATTENDANCES_VIEW = 'attendances.view';
case ATTENDANCES_CREATE = 'attendances.create';
case ATTENDANCES_DELETE = 'attendances.delete';
case ATTENDANCES_MANAGE = 'attendances.manage';
// Leave Requests
case LEAVE_REQUESTS_VIEW = 'leave_requests.view';
case LEAVE_REQUESTS_CREATE = 'leave_requests.create';
case LEAVE_REQUESTS_UPDATE = 'leave_requests.update';
case LEAVE_REQUESTS_DELETE = 'leave_requests.delete';
case LEAVE_REQUESTS_VERIFY = 'leave_requests.verify';
// Categories
case CATEGORIES_VIEW = 'categories.view';
case CATEGORIES_CREATE = 'categories.create';
case CATEGORIES_UPDATE = 'categories.update';
case CATEGORIES_DELETE = 'categories.delete';
// Customers
case CUSTOMERS_VIEW = 'customers.view';
case CUSTOMERS_CREATE = 'customers.create';
case CUSTOMERS_UPDATE = 'customers.update';
case CUSTOMERS_DELETE = 'customers.delete';
// Products
case PRODUCTS_VIEW = 'products.view';
case PRODUCTS_CREATE = 'products.create';
case PRODUCTS_UPDATE = 'products.update';
case PRODUCTS_DELETE = 'products.delete';
case PRODUCTS_TOGGLE_STATUS = 'products.toggle_status';
// Stocks
case STOCKS_VIEW = 'stocks.view';
// Orders
case ORDERS_VIEW = 'orders.view';
case ORDERS_CREATE = 'orders.create';
case ORDERS_UPDATE = 'orders.update';
case ORDERS_DELETE = 'orders.delete';
case ORDERS_SEND = 'orders.send';
case ORDERS_COMPLETE = 'orders.complete';
case ORDERS_CANCEL = 'orders.cancel';
// Cuttings
case CUTTINGS_VIEW = 'cuttings.view';
case CUTTINGS_CREATE = 'cuttings.create';
case CUTTINGS_UPDATE = 'cuttings.update';
case CUTTINGS_DELETE = 'cuttings.delete';
case CUTTINGS_COMPLETE = 'cuttings.complete';
// Cash
case CASH_VIEW = 'cash.view';
case CASH_DEPOSIT = 'cash.deposit';
case CASH_WITHDRAW = 'cash.withdraw';
case CASH_UPDATE = 'cash.update';
case CASH_DELETE = 'cash.delete';
// Expenses
case EXPENSES_VIEW = 'expenses.view';
case EXPENSES_CREATE = 'expenses.create';
case EXPENSES_UPDATE = 'expenses.update';
case EXPENSES_DELETE = 'expenses.delete';
// Activity Logs
case ACTIVITY_LOGS_VIEW = 'activity_logs.view';
// Employee Advances
case EMPLOYEE_ADVANCES_VIEW = 'employee_advances.view';
case EMPLOYEE_ADVANCES_CREATE = 'employee_advances.create';
case EMPLOYEE_ADVANCES_UPDATE = 'employee_advances.update';
case EMPLOYEE_ADVANCES_DELETE = 'employee_advances.delete';
case EMPLOYEE_ADVANCES_PAY = 'employee_advances.pay';
case EMPLOYEE_ADVANCES_VERIFY = 'employee_advances.verify';
// Payroll
case PAYROLL_VIEW = 'payroll.view';
case PAYROLL_PAY = 'payroll.pay';
case PAYROLL_CANCEL = 'payroll.cancel';
case PAYROLL_ADJUST = 'payroll.adjust';
// Owner Verifications
case OWNER_VERIFICATIONS_VIEW = 'owner_verifications.view';
case OWNER_VERIFICATIONS_VERIFY = 'owner_verifications.verify';
case OWNER_VERIFICATIONS_REJECT = 'owner_verifications.reject';
// Restocks
case RESTOCKS_VIEW = 'restocks.view';
case RESTOCKS_CREATE = 'restocks.create';
case RESTOCKS_UPDATE = 'restocks.update';
case RESTOCKS_DELETE = 'restocks.delete';
// Raw Materials
case RAW_MATERIALS_VIEW = 'raw_materials.view';
case RAW_MATERIALS_CREATE = 'raw_materials.create';
case RAW_MATERIALS_UPDATE = 'raw_materials.update';
case RAW_MATERIALS_DELETE = 'raw_materials.delete';
case RAW_MATERIALS_TOGGLE_STATUS = 'raw_materials.toggle_status';
// Suppliers
case SUPPLIERS_VIEW = 'suppliers.view';
case SUPPLIERS_CREATE = 'suppliers.create';
case SUPPLIERS_UPDATE = 'suppliers.update';
case SUPPLIERS_DELETE = 'suppliers.delete';
// Purchases
case PURCHASES_VIEW = 'purchases.view';
case PURCHASES_CREATE = 'purchases.create';
case PURCHASES_UPDATE = 'purchases.update';
case PURCHASES_DELETE = 'purchases.delete';
// Stok Opnames
case STOK_OPNAMES_VIEW = 'stok_opnames.view';
case STOK_OPNAMES_CREATE = 'stok_opnames.create';
case STOK_OPNAMES_UPDATE = 'stok_opnames.update';
case STOK_OPNAMES_DELETE = 'stok_opnames.delete';
case STOK_OPNAMES_SUBMIT = 'stok_opnames.submit';
case STOK_OPNAMES_VERIFY = 'stok_opnames.verify';
// Roles
case ROLES_VIEW = 'roles.view';
case ROLES_CREATE = 'roles.create';
case ROLES_UPDATE = 'roles.update';
case ROLES_DELETE = 'roles.delete';
// Settings
case SETTINGS_VIEW_SYSTEM = 'settings.view_system';
case SETTINGS_UPDATE_SYSTEM = 'settings.update_system';
case SETTINGS_VIEW_HOMEPAGE = 'settings.view_homepage';
case SETTINGS_UPDATE_HOMEPAGE = 'settings.update_homepage';
case SETTINGS_VIEW_SOCIAL_MEDIA = 'settings.view_social_media';
case SETTINGS_UPDATE_SOCIAL_MEDIA = 'settings.update_social_media';
case SETTINGS_VIEW_MARKETPLACE = 'settings.view_marketplace';
case SETTINGS_UPDATE_MARKETPLACE = 'settings.update_marketplace';
case SETTINGS_VIEW_HR = 'settings.view_hr';
case SETTINGS_UPDATE_HR = 'settings.update_hr';
public function label(): string
{
return $this->value;
}
}

View File

@ -11,4 +11,13 @@ enum RawMaterialUnit: string
case KG = 'kg'; case KG = 'kg';
case METER = 'meter'; case METER = 'meter';
case YARD = 'yard'; case YARD = 'yard';
public function label(): string
{
return match ($this) {
self::KG => 'Kg',
self::METER => 'Meter',
self::YARD => 'Yard',
};
}
} }

458
app/Enums/Role.php Normal file
View File

@ -0,0 +1,458 @@
<?php
namespace App\Enums;
use App\Traits\ProvidesEnumOptions;
enum Role: string
{
use ProvidesEnumOptions;
case DEVELOPER = 'developer';
case OWNER = 'owner';
case ADMIN_TOKO = 'admin-toko';
case ADMIN_BAHAN_BAKU = 'admin-bahan-baku';
case DIREKTUR = 'direktur';
case MARKETING_OFFLINE = 'marketing-offline';
case MARKETING_ONLINE = 'marketing-online';
case CASHIER = 'cashier';
case NON_OPERATOR = 'non-operator';
case STOK_OPNAME = 'stok-opname';
public function label(): string
{
return match ($this) {
self::DEVELOPER => 'Developer',
self::OWNER => 'Owner',
self::ADMIN_TOKO => 'Admin Toko',
self::ADMIN_BAHAN_BAKU => 'Admin Bahan Baku',
self::DIREKTUR => 'Direktur',
self::MARKETING_OFFLINE => 'Marketing Offline',
self::MARKETING_ONLINE => 'Marketing Online',
self::CASHIER => 'Kasir',
self::NON_OPERATOR => 'Non Operator',
self::STOK_OPNAME => 'Stok Opname',
};
}
public function permissions(): array
{
return match ($this) {
self::DEVELOPER, self::OWNER => array_values(array_filter(
Permission::cases(),
fn (Permission $permission) => ! in_array($permission, [
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
Permission::STOK_OPNAMES_SUBMIT,
Permission::ROLES_VIEW,
Permission::ROLES_CREATE,
Permission::ROLES_UPDATE,
Permission::ROLES_DELETE,
], true)
)),
self::DIREKTUR => [
Permission::DASHBOARD_VIEW,
Permission::DASHBOARD_ATTENDANCE,
Permission::DASHBOARD_REVENUE,
Permission::DASHBOARD_EXPENSE,
Permission::DASHBOARD_ORDERS_CHANNEL,
Permission::DASHBOARD_ORDERS_PAYMENT,
Permission::DASHBOARD_ORDERS_MARKETING,
Permission::DASHBOARD_ORDERS_STATUS,
Permission::ANALYSIS_VIEW,
Permission::ANALYSIS_ATTENDANCE,
Permission::ANALYSIS_CASH,
Permission::ANALYSIS_PRODUCT_STOCK,
Permission::ANALYSIS_REVENUE,
Permission::ANALYSIS_EXPENSE,
Permission::ANALYSIS_BUSY_HOURS,
Permission::ANALYSIS_PROFIT_ORDERS,
Permission::ANALYSIS_PROFIT_HPP,
Permission::ANALYSIS_PROFIT_GROSS,
Permission::ANALYSIS_PROFIT_MARGIN,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ANALYSIS_MARKETING_SALES,
Permission::EMPLOYEES_VIEW,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
Permission::ATTENDANCES_MANAGE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::CATEGORIES_VIEW,
Permission::CUSTOMERS_VIEW,
Permission::PRODUCTS_VIEW,
Permission::STOCKS_VIEW,
Permission::ORDERS_VIEW,
Permission::CUTTINGS_VIEW,
Permission::CASH_VIEW,
Permission::EXPENSES_VIEW,
Permission::ACTIVITY_LOGS_VIEW,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
],
self::ADMIN_TOKO => [
Permission::DASHBOARD_VIEW,
Permission::DASHBOARD_REVENUE,
Permission::DASHBOARD_EXPENSE,
Permission::DASHBOARD_ORDERS_CHANNEL,
Permission::DASHBOARD_ORDERS_PAYMENT,
Permission::DASHBOARD_ORDERS_MARKETING,
Permission::DASHBOARD_ORDERS_STATUS,
Permission::ANALYSIS_VIEW,
Permission::ANALYSIS_ATTENDANCE,
Permission::ANALYSIS_CASH,
Permission::ANALYSIS_PRODUCT_STOCK,
Permission::ANALYSIS_REVENUE,
Permission::ANALYSIS_EXPENSE,
Permission::ANALYSIS_BUSY_HOURS,
Permission::ANALYSIS_PROFIT_ORDERS,
Permission::ANALYSIS_PROFIT_HPP,
Permission::ANALYSIS_PROFIT_GROSS,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ANALYSIS_MARKETING_SALES,
Permission::STOCKS_VIEW,
Permission::STOK_OPNAMES_VIEW,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::RESTOCKS_VIEW,
Permission::RESTOCKS_CREATE,
Permission::RESTOCKS_UPDATE,
Permission::RESTOCKS_DELETE,
Permission::CUSTOMERS_VIEW,
Permission::CUSTOMERS_CREATE,
Permission::CUSTOMERS_UPDATE,
Permission::CUSTOMERS_DELETE,
Permission::PRODUCTS_VIEW,
Permission::PRODUCTS_CREATE,
Permission::PRODUCTS_UPDATE,
Permission::PRODUCTS_DELETE,
Permission::PRODUCTS_TOGGLE_STATUS,
Permission::STOCKS_VIEW,
Permission::OWNER_VERIFICATIONS_VIEW,
Permission::ORDERS_VIEW,
Permission::ORDERS_CREATE,
Permission::ORDERS_UPDATE,
Permission::ORDERS_DELETE,
Permission::ORDERS_SEND,
Permission::ORDERS_COMPLETE,
Permission::ORDERS_CANCEL,
Permission::CASH_VIEW,
Permission::CASH_DEPOSIT,
Permission::CASH_WITHDRAW,
Permission::CASH_UPDATE,
Permission::CASH_DELETE,
Permission::EXPENSES_VIEW,
Permission::EXPENSES_CREATE,
Permission::EXPENSES_UPDATE,
Permission::EXPENSES_DELETE,
Permission::ACTIVITY_LOGS_VIEW,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
Permission::PAYROLL_ADJUST,
Permission::SETTINGS_VIEW_SYSTEM,
Permission::SETTINGS_UPDATE_SYSTEM,
Permission::SETTINGS_VIEW_SOCIAL_MEDIA,
Permission::SETTINGS_UPDATE_SOCIAL_MEDIA,
Permission::SETTINGS_VIEW_MARKETPLACE,
Permission::SETTINGS_UPDATE_MARKETPLACE,
Permission::SETTINGS_VIEW_HR,
Permission::SETTINGS_UPDATE_HR,
Permission::SETTINGS_VIEW_HOMEPAGE,
Permission::SETTINGS_UPDATE_HOMEPAGE,
Permission::EMPLOYEES_VIEW,
Permission::EMPLOYEES_CREATE,
Permission::EMPLOYEES_UPDATE,
Permission::EMPLOYEES_TOGGLE_STATUS,
Permission::EMPLOYEES_DELETE,
],
self::CASHIER => [
Permission::DASHBOARD_VIEW,
Permission::DASHBOARD_REVENUE,
Permission::DASHBOARD_ORDERS_CHANNEL,
Permission::DASHBOARD_ORDERS_PAYMENT,
Permission::DASHBOARD_ORDERS_STATUS,
Permission::ANALYSIS_VIEW,
Permission::ANALYSIS_ATTENDANCE,
Permission::ANALYSIS_REVENUE,
Permission::ANALYSIS_PROFIT_ORDERS,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::CUSTOMERS_VIEW,
Permission::CUSTOMERS_CREATE,
Permission::CUSTOMERS_UPDATE,
Permission::CUSTOMERS_DELETE,
Permission::PRODUCTS_VIEW,
Permission::ORDERS_VIEW,
Permission::ORDERS_CREATE,
Permission::ORDERS_UPDATE,
Permission::ORDERS_DELETE,
Permission::ORDERS_SEND,
Permission::ORDERS_COMPLETE,
Permission::ORDERS_CANCEL,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
Permission::PAYROLL_ADJUST,
],
self::ADMIN_BAHAN_BAKU => [
Permission::DASHBOARD_VIEW,
Permission::EMPLOYEES_VIEW,
Permission::EMPLOYEES_CREATE,
Permission::EMPLOYEES_UPDATE,
Permission::EMPLOYEES_TOGGLE_STATUS,
Permission::EMPLOYEES_DELETE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::CATEGORIES_VIEW,
Permission::CATEGORIES_CREATE,
Permission::CATEGORIES_UPDATE,
Permission::CATEGORIES_DELETE,
Permission::SUPPLIERS_VIEW,
Permission::SUPPLIERS_CREATE,
Permission::SUPPLIERS_UPDATE,
Permission::SUPPLIERS_DELETE,
Permission::RAW_MATERIALS_VIEW,
Permission::RAW_MATERIALS_CREATE,
Permission::RAW_MATERIALS_UPDATE,
Permission::RAW_MATERIALS_TOGGLE_STATUS,
Permission::STOCKS_VIEW,
Permission::OWNER_VERIFICATIONS_VIEW,
Permission::PRODUCTS_VIEW,
Permission::PRODUCTS_CREATE,
Permission::PRODUCTS_UPDATE,
Permission::PRODUCTS_DELETE,
Permission::PRODUCTS_TOGGLE_STATUS,
Permission::CUTTINGS_VIEW,
Permission::CUTTINGS_CREATE,
Permission::CUTTINGS_UPDATE,
Permission::CUTTINGS_DELETE,
Permission::CUTTINGS_COMPLETE,
Permission::PURCHASES_VIEW,
Permission::PURCHASES_CREATE,
Permission::PURCHASES_UPDATE,
Permission::PURCHASES_DELETE,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::ACTIVITY_LOGS_VIEW,
Permission::PAYROLL_VIEW,
],
self::MARKETING_OFFLINE, self::MARKETING_ONLINE => [
Permission::DASHBOARD_VIEW,
Permission::DASHBOARD_REVENUE,
Permission::DASHBOARD_ORDERS_CHANNEL,
Permission::DASHBOARD_ORDERS_PAYMENT,
Permission::DASHBOARD_ORDERS_MARKETING,
Permission::DASHBOARD_ORDERS_STATUS,
Permission::ANALYSIS_VIEW,
Permission::ANALYSIS_ATTENDANCE,
Permission::ANALYSIS_REVENUE,
Permission::ANALYSIS_PROFIT_ORDERS,
Permission::ANALYSIS_TOP_PRODUCTS,
Permission::ANALYSIS_TOP_CUSTOMERS,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::PRODUCTS_VIEW,
Permission::ORDERS_VIEW,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
],
self::NON_OPERATOR => [
Permission::DASHBOARD_VIEW,
Permission::ANALYSIS_VIEW,
Permission::ANALYSIS_ATTENDANCE,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
],
self::STOK_OPNAME => [
Permission::DASHBOARD_VIEW,
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::LEAVE_REQUESTS_VIEW,
Permission::LEAVE_REQUESTS_CREATE,
Permission::LEAVE_REQUESTS_UPDATE,
Permission::LEAVE_REQUESTS_DELETE,
Permission::PRODUCTS_VIEW,
Permission::STOCKS_VIEW,
Permission::STOK_OPNAMES_VIEW,
Permission::STOK_OPNAMES_CREATE,
Permission::STOK_OPNAMES_UPDATE,
Permission::STOK_OPNAMES_DELETE,
Permission::STOK_OPNAMES_SUBMIT,
Permission::EMPLOYEE_ADVANCES_VIEW,
Permission::EMPLOYEE_ADVANCES_CREATE,
Permission::EMPLOYEE_ADVANCES_UPDATE,
Permission::EMPLOYEE_ADVANCES_DELETE,
Permission::EMPLOYEE_ADVANCES_PAY,
Permission::PAYROLL_VIEW,
],
};
}
public function isAssignable(): bool
{
return $this !== self::DEVELOPER;
}
/**
* @return list<array{value: string, label: string}>
*/
public static function assignableSelectOptions(): array
{
return collect(self::cases())
->filter(fn (self $role) => $role->isAssignable())
->map(fn (self $role) => [
'value' => $role->value,
'label' => $role->label(),
])
->values()
->all();
}
/**
* @return list<string>
*/
public static function assignableValues(): array
{
return collect(self::cases())
->filter(fn (self $role) => $role->isAssignable())
->map(fn (self $role) => $role->value)
->values()
->all();
}
/**
* @return list<string>
*/
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@ -8,9 +8,20 @@ enum StokOpnameStatus: string
{ {
use HasValues; use HasValues;
case CANCELLED = 'cancelled';
case COMPLETED = 'completed';
case DRAFT = 'draft'; case DRAFT = 'draft';
case IN_PROGRESS = 'in_progress'; case IN_PROGRESS = 'in_progress';
case COMPLETED = 'completed';
case VERIFIED = 'verified'; case VERIFIED = 'verified';
case CANCELLED = 'cancelled';
public function label(): string
{
return match ($this) {
self::CANCELLED => 'Dibatalkan',
self::COMPLETED => 'Selesai',
self::DRAFT => 'Draft',
self::IN_PROGRESS => 'Dalam Proses',
self::VERIFIED => 'Terverifikasi',
};
}
} }

View File

@ -16,7 +16,7 @@
class AdminSettingsController extends Controller class AdminSettingsController extends Controller
{ {
public function __construct( public function __construct(
private AdminSettingsService $service private readonly AdminSettingsService $service
) {} ) {}
public function index(): Response public function index(): Response
@ -70,7 +70,7 @@ public function updateHR(UpdateHRRequest $request): RedirectResponse
{ {
$this->service->updateHR($request->validated()); $this->service->updateHR($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan hr berhasil diperbarui.']); Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan HR berhasil diperbarui.']);
return to_route('admin.settings.index'); return to_route('admin.settings.index');
} }

View File

@ -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;
@ -14,7 +15,7 @@
class CashAccountController extends Controller class CashAccountController extends Controller
{ {
public function __construct( public function __construct(
private CashAccountService $service private readonly CashAccountService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response
@ -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(),
],
]); ]);
} }

View File

@ -14,7 +14,7 @@
class EmployeeAdvanceController extends Controller class EmployeeAdvanceController extends Controller
{ {
public function __construct( public function __construct(
private EmployeeAdvanceService $service private readonly EmployeeAdvanceService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class ExpenseController extends Controller class ExpenseController extends Controller
{ {
public function __construct( public function __construct(
private ExpenseService $service private readonly ExpenseService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -12,7 +12,7 @@
class PayrollAdjustmentController extends Controller class PayrollAdjustmentController extends Controller
{ {
public function __construct( public function __construct(
private PayrollAdjustmentService $service private readonly PayrollAdjustmentService $service
) {} ) {}
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
@ -21,7 +21,7 @@ public function store(PayrollAdjustmentRequest $request, Payroll $payroll): Redi
fn () => $this->service->create($payroll, $request->validated()), fn () => $this->service->create($payroll, $request->validated()),
'Adjustment gaji berhasil ditambahkan.', 'Adjustment gaji berhasil ditambahkan.',
'admin.finance.payroll-periods.show', 'admin.finance.payroll-periods.show',
['payroll_period' => $payroll->payroll_period_id] parameters : ['payroll_period' => $payroll->payroll_period_id]
); );
} }
@ -31,7 +31,7 @@ public function destroy(PayrollAdjustment $payrollAdjustment): RedirectResponse
fn () => $this->service->delete($payrollAdjustment), fn () => $this->service->delete($payrollAdjustment),
'Adjustment gaji berhasil dihapus.', 'Adjustment gaji berhasil dihapus.',
'admin.finance.payroll-periods.show', 'admin.finance.payroll-periods.show',
['payroll_period' => $payrollAdjustment->payroll->payroll_period_id] parameters: ['payroll_period' => $payrollAdjustment->payroll->payroll_period_id]
); );
} }
} }

View File

@ -10,7 +10,7 @@
class PayrollController extends Controller class PayrollController extends Controller
{ {
public function __construct( public function __construct(
private PayrollPeriodService $service private readonly PayrollPeriodService $service
) {} ) {}
public function pay(Payroll $payroll): RedirectResponse public function pay(Payroll $payroll): RedirectResponse

View File

@ -14,7 +14,7 @@
class PayrollPeriodController extends Controller class PayrollPeriodController extends Controller
{ {
public function __construct( public function __construct(
private PayrollPeriodService $service private readonly PayrollPeriodService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -15,7 +15,7 @@
class AttendanceController extends Controller class AttendanceController extends Controller
{ {
public function __construct( public function __construct(
private AttendanceService $service private readonly AttendanceService $service
) {} ) {}
public function index(Request $request): Response public function index(Request $request): Response

View File

@ -10,11 +10,12 @@
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
use Spatie\Permission\Models\Role;
class EmployeeController extends Controller class EmployeeController extends Controller
{ {
public function __construct( public function __construct(
private EmployeeService $service private readonly EmployeeService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response
@ -30,13 +31,15 @@ public function index(PaginatedRequest $request): Response
public function create(): Response public function create(): Response
{ {
return Inertia::render('admin/hr/employee/create'); return Inertia::render('admin/hr/employee/create', [
'roles' => Role::where('name', '!=', 'Developer')->get(['id', 'name']),
]);
} }
public function store(EmployeeRequest $request): RedirectResponse public function store(EmployeeRequest $request): RedirectResponse
{ {
return $this->handleAction( return $this->handleAction(
fn () => $this->service->create($request->validated()), fn() => $this->service->create($request->validated()),
'Pegawai berhasil ditambahkan.', 'Pegawai berhasil ditambahkan.',
'admin.hr.employees.index' 'admin.hr.employees.index'
); );
@ -44,17 +47,18 @@ public function store(EmployeeRequest $request): RedirectResponse
public function edit(User $user): Response public function edit(User $user): Response
{ {
$user->load(['userProfile', 'employee']); $user->load(['userProfile', 'employee', 'roles']);
return Inertia::render('admin/hr/employee/edit', [ return Inertia::render('admin/hr/employee/edit', [
'employee' => $user, 'employee' => $user,
'roles' => Role::where('name', '!=', 'Developer')->get(['id', 'name']),
]); ]);
} }
public function update(EmployeeRequest $request, User $user): RedirectResponse public function update(EmployeeRequest $request, User $user): RedirectResponse
{ {
return $this->handleAction( return $this->handleAction(
fn () => $this->service->update($user, $request->validated()), fn() => $this->service->update($user, $request->validated()),
'Pegawai berhasil diperbarui.', 'Pegawai berhasil diperbarui.',
'admin.hr.employees.index' 'admin.hr.employees.index'
); );
@ -63,7 +67,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
public function destroy(User $user): RedirectResponse public function destroy(User $user): RedirectResponse
{ {
return $this->handleAction( return $this->handleAction(
fn () => $this->service->delete($user), fn() => $this->service->delete($user),
'Pegawai berhasil dihapus.', 'Pegawai berhasil dihapus.',
'admin.hr.employees.index' 'admin.hr.employees.index'
); );
@ -82,7 +86,7 @@ public function toggleActive(User $user): RedirectResponse
public function resetPassword(User $user): RedirectResponse public function resetPassword(User $user): RedirectResponse
{ {
return $this->handleAction( return $this->handleAction(
fn () => $this->service->resetPassword($user), fn() => $this->service->resetPassword($user),
'Kata sandi pegawai berhasil direset ke kata sandi default.', 'Kata sandi pegawai berhasil direset ke kata sandi default.',
'admin.hr.employees.index' 'admin.hr.employees.index'
); );

View File

@ -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;
@ -14,7 +15,7 @@
class LeaveRequestController extends Controller class LeaveRequestController extends Controller
{ {
public function __construct( public function __construct(
private LeaveRequestService $service private readonly LeaveRequestService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response
@ -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(),
],
]); ]);
} }

View File

@ -14,7 +14,7 @@
class CuttingController extends Controller class CuttingController extends Controller
{ {
public function __construct( public function __construct(
private CuttingService $service, private readonly CuttingService $service,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class PurchaseController extends Controller class PurchaseController extends Controller
{ {
public function __construct( public function __construct(
private PurchaseService $service, private readonly PurchaseService $service,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class RestockController extends Controller class RestockController extends Controller
{ {
public function __construct( public function __construct(
private RestockService $service, private readonly RestockService $service,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class TransactionController extends Controller class TransactionController extends Controller
{ {
public function __construct( public function __construct(
private TransactionService $service, private readonly TransactionService $service,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class CategoryController extends Controller class CategoryController extends Controller
{ {
public function __construct( public function __construct(
private CategoryService $service private readonly CategoryService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class CustomerController extends Controller class CustomerController extends Controller
{ {
public function __construct( public function __construct(
private CustomerService $service private readonly CustomerService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -15,8 +15,8 @@
class ProductController extends Controller class ProductController extends Controller
{ {
public function __construct( public function __construct(
private ProductService $service, private readonly ProductService $service,
private CategoryService $categoryService, private readonly CategoryService $categoryService,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -15,7 +15,7 @@
class ProductVariantController extends Controller class ProductVariantController extends Controller
{ {
public function __construct( public function __construct(
private ProductVariantService $variantService, private readonly ProductVariantService $variantService,
) {} ) {}
public function edit(Product $product, ProductVariant $variant): Response public function edit(Product $product, ProductVariant $variant): Response

View File

@ -13,7 +13,7 @@
class StockMutationController extends Controller class StockMutationController extends Controller
{ {
public function __construct( public function __construct(
private StockMutationService $service = new StockMutationService, private readonly StockMutationService $service,
) {} ) {}
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response

View File

@ -14,7 +14,7 @@
class RawMaterialController extends Controller class RawMaterialController extends Controller
{ {
public function __construct( public function __construct(
private RawMaterialService $service, private readonly RawMaterialService $service,
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class RawMaterialVariantController extends Controller class RawMaterialVariantController extends Controller
{ {
public function __construct( public function __construct(
private RawMaterialVariantService $variantService, private readonly RawMaterialVariantService $variantService,
) {} ) {}
public function edit(RawMaterial $rawMaterial, RawMaterialPrice $variant): Response public function edit(RawMaterial $rawMaterial, RawMaterialPrice $variant): Response

View File

@ -14,7 +14,7 @@
class SupplierController extends Controller class SupplierController extends Controller
{ {
public function __construct( public function __construct(
private SupplierService $service private readonly SupplierService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -14,7 +14,7 @@
class RoleController extends Controller class RoleController extends Controller
{ {
public function __construct( public function __construct(
private RoleService $service private readonly RoleService $service
) {} ) {}
public function index(PaginatedRequest $request): Response public function index(PaginatedRequest $request): Response

View File

@ -11,7 +11,7 @@
class PresignedUrlController extends Controller class PresignedUrlController extends Controller
{ {
public function __construct( public function __construct(
private S3PresignedService $service private readonly S3PresignedService $service
) {} ) {}
public function store(PresignedUrlRequest $request): JsonResponse public function store(PresignedUrlRequest $request): JsonResponse

View File

@ -28,4 +28,9 @@ protected function handleAction(callable $action, string $successMessage, string
return to_route($errorRoute ?? $redirectRoute, $parameters); return to_route($errorRoute ?? $redirectRoute, $parameters);
} }
} }
protected function handleToggle(callable $action, string $successMessage, string $redirectRoute): RedirectResponse
{
return $this->handleAction($action, $successMessage, $redirectRoute);
}
} }

View File

@ -12,9 +12,6 @@
class ProfileController extends Controller class ProfileController extends Controller
{ {
/**
* Show the user's profile settings page.
*/
public function edit(Request $request): Response public function edit(Request $request): Response
{ {
$user = $request->user(); $user = $request->user();
@ -38,38 +35,37 @@ public function edit(Request $request): Response
]); ]);
} }
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse public function update(ProfileUpdateRequest $request): RedirectResponse
{ {
$validated = $request->validated(); return $this->handleAction(
$user = $request->user(); function () use ($request) {
$validated = $request->validated();
$user = $request->user();
$user->fill([ $user->fill([
'email' => $validated['email'], 'email' => $validated['email'],
'username' => $validated['username'], 'username' => $validated['username'],
]); ]);
if ($user->isDirty('email')) { if ($user->isDirty('email')) {
$user->email_verified_at = null; $user->email_verified_at = null;
} }
$user->save(); $user->save();
$user->userProfile()->updateOrCreate( $user->userProfile()->updateOrCreate(
[], [],
[ [
'full_name' => $validated['full_name'], 'full_name' => $validated['full_name'],
'phone_number' => $validated['phone_number'] ?? null, 'phone_number' => $validated['phone_number'] ?? null,
'gender' => $validated['gender'] ?? null, 'gender' => $validated['gender'] ?? null,
'birth_date' => $validated['birth_date'] ?? null, 'birth_date' => $validated['birth_date'] ?? null,
'address' => $validated['address'] ?? null, 'address' => $validated['address'] ?? null,
], ],
);
},
'Profil berhasil diperbarui.',
'profile.edit',
); );
Inertia::flash('toast', ['type' => 'success', 'message' => 'Profil berhasil diperbarui.']);
return to_route('profile.edit');
} }
} }

View File

@ -11,26 +11,16 @@
class SecurityController extends Controller class SecurityController extends Controller
{ {
/**
* Show the user's security settings page.
*/
public function edit(): Response public function edit(): Response
{ {
$props = [ return Inertia::render('settings/security', [
'passwordRules' => Password::defaults()->toPasswordRulesString(), 'passwordRules' => Password::defaults()->toPasswordRulesString(),
]; ]);
return Inertia::render('settings/security', $props);
} }
/**
* Update the user's password.
*/
public function update(PasswordUpdateRequest $request): RedirectResponse public function update(PasswordUpdateRequest $request): RedirectResponse
{ {
$request->user()->update([ $request->user()->update(['password' => $request->password]);
'password' => $request->password,
]);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.']); Inertia::flash('toast', ['type' => 'success', 'message' => 'Kata sandi berhasil diperbarui.']);

View File

@ -40,7 +40,9 @@ public function share(Request $request): array
'name' => config('app.name'), 'name' => config('app.name'),
'auth' => [ 'auth' => [
'user' => $request->user() 'user' => $request->user()
? $request->user()->load('userProfile', 'roles', 'permissions') ? tap($request->user()->load('userProfile', 'roles'), function ($user) {
$user->setRelation('permissions', $user->getAllPermissions());
})
: null, : null,
], ],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',

View File

@ -2,24 +2,23 @@
namespace App\Http\Requests\Admin\Finance; namespace App\Http\Requests\Admin\Finance;
use App\Concerns\CurrencyStripping;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Override; use Override;
class CashTransactionRequest extends FormRequest class CashTransactionRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
if ($this->has('amount')) { $this->merge($this->stripCurrencyDot($this->all(), 'amount'));
$this->merge([
'amount' => str_replace('.', '', $this->amount),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -2,24 +2,23 @@
namespace App\Http\Requests\Admin\Finance; namespace App\Http\Requests\Admin\Finance;
use App\Concerns\CurrencyStripping;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Override; use Override;
class EmployeeAdvanceRequest extends FormRequest class EmployeeAdvanceRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
if ($this->has('amount')) { $this->merge($this->stripCurrencyDot($this->all(), 'amount'));
$this->merge([
'amount' => str_replace('.', '', $this->amount),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -2,24 +2,23 @@
namespace App\Http\Requests\Admin\Finance; namespace App\Http\Requests\Admin\Finance;
use App\Concerns\CurrencyStripping;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Override; use Override;
class ExpenseRequest extends FormRequest class ExpenseRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
if ($this->has('amount')) { $this->merge($this->stripCurrencyDot($this->all(), 'amount'));
$this->merge([
'amount' => str_replace('.', '', $this->amount),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin\Finance; namespace App\Http\Requests\Admin\Finance;
use App\Concerns\CurrencyStripping;
use App\Enums\PayrollAdjustmentType; use App\Enums\PayrollAdjustmentType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -9,19 +10,17 @@
class PayrollAdjustmentRequest extends FormRequest class PayrollAdjustmentRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
if ($this->has('amount')) { $this->merge($this->stripCurrencyDot($this->all(), 'amount'));
$this->merge([
'amount' => str_replace('.', '', $this->amount),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -15,6 +15,7 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
$userId = $this->route('user')?->id; $userId = $this->route('user')?->id;
$isOwner = $this->input('role') === 'owner';
return [ return [
'email' => [ 'email' => [
@ -30,15 +31,30 @@ public function rules(): array
'alpha_dash', 'alpha_dash',
Rule::unique('users', 'username')->ignore($userId, 'id'), Rule::unique('users', 'username')->ignore($userId, 'id'),
], ],
'role' => ['required', 'string', Rule::exists('roles', 'name')],
'full_name' => ['required', 'string', 'max:200'], 'full_name' => ['required', 'string', 'max:200'],
'phone_number' => ['nullable', 'string', 'max:20'], 'phone_number' => ['nullable', 'string', 'max:20'],
'gender' => ['nullable', 'in:male,female'], 'gender' => ['nullable', 'in:male,female'],
'birth_date' => ['nullable', 'date'], 'birth_date' => ['nullable', 'date'],
'address' => ['nullable', 'string'], 'address' => ['nullable', 'string'],
'join_date' => ['required', 'date'], 'join_date' => [
'resign_date' => ['nullable', 'date', 'after_or_equal:join_date'], 'date',
'employment_status' => ['required', Rule::in(['full_time', 'part_time', 'contract', 'internship', 'resigned'])], Rule::requiredIf(! $isOwner),
'base_salary' => ['required', 'integer', 'min:0'], ],
'resign_date' => [
'nullable',
'date',
'after_or_equal:join_date',
],
'employment_status' => [
Rule::requiredIf(! $isOwner),
Rule::in(['full_time', 'part_time', 'contract', 'internship', 'resigned']),
],
'base_salary' => [
Rule::requiredIf(! $isOwner),
'integer',
'min:0',
],
]; ];
} }
@ -47,6 +63,7 @@ public function attributes(): array
return [ return [
'email' => 'Email', 'email' => 'Email',
'username' => 'Username', 'username' => 'Username',
'role' => 'Role',
'full_name' => 'Nama Lengkap', 'full_name' => 'Nama Lengkap',
'phone_number' => 'No. Telepon', 'phone_number' => 'No. Telepon',
'gender' => 'Jenis Kelamin', 'gender' => 'Jenis Kelamin',

View File

@ -2,12 +2,15 @@
namespace App\Http\Requests\Admin\Manage; namespace App\Http\Requests\Admin\Manage;
use App\Concerns\CurrencyStripping;
use App\Enums\RawMaterialUnit; use App\Enums\RawMaterialUnit;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class PurchaseRequest extends FormRequest class PurchaseRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
@ -15,22 +18,7 @@ public function authorize(): bool
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
$variants = $this->variants; $this->merge($this->stripCurrencyDot($this->all(), 'variants.*.price', 'discount', 'shipping_cost'));
if (is_array($variants)) {
foreach ($variants as $i => $variant) {
if (isset($variant['price']) && is_string($variant['price'])) {
$this->request->set("variants.$i.price", (int) str_replace('.', '', $variant['price']));
}
}
}
if (is_string($this->discount)) {
$this->request->set('discount', (int) str_replace('.', '', $this->discount));
}
if (is_string($this->shipping_cost)) {
$this->request->set('shipping_cost', (int) str_replace('.', '', $this->shipping_cost));
}
} }
public function rules(): array public function rules(): array

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin\Manage; namespace App\Http\Requests\Admin\Manage;
use App\Concerns\CurrencyStripping;
use App\Enums\OrderChannel; use App\Enums\OrderChannel;
use App\Enums\PaymentType; use App\Enums\PaymentType;
use App\Enums\PriceType; use App\Enums\PriceType;
@ -11,6 +12,8 @@
class TransactionRequest extends FormRequest class TransactionRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
@ -18,17 +21,7 @@ public function authorize(): bool
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
if ($this->has('discount')) { $this->merge($this->stripCurrencyDot($this->all(), 'discount', 'nego_price'));
$this->merge([
'discount' => str_replace('.', '', $this->discount),
]);
}
if ($this->has('nego_price')) {
$this->merge([
'nego_price' => str_replace('.', '', $this->nego_price),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin\Master\Product; namespace App\Http\Requests\Admin\Master\Product;
use App\Concerns\CurrencyStripping;
use App\Enums\PriceType; use App\Enums\PriceType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -9,35 +10,17 @@
class ProductRequest extends FormRequest class ProductRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
$sharedPrices = $this->shared_prices; $this->merge($this->stripCurrencyDot($this->all(), 'shared_prices.*.price', 'variants.*.prices.*.price'));
if (is_array($sharedPrices)) {
foreach ($sharedPrices as $i => $price) {
if (isset($price['price']) && is_string($price['price'])) {
$this->request->set("shared_prices.$i.price", (int) str_replace('.', '', $price['price']));
}
}
}
$variants = $this->variants;
if (is_array($variants)) {
foreach ($variants as $i => $variant) {
if (isset($variant['prices']) && is_array($variant['prices'])) {
foreach ($variant['prices'] as $j => $price) {
if (isset($price['price']) && is_string($price['price'])) {
$this->request->set("variants.$i.prices.$j.price", (int) str_replace('.', '', $price['price']));
}
}
}
}
}
} }
public function rules(): array public function rules(): array

View File

@ -2,12 +2,15 @@
namespace App\Http\Requests\Admin\Master\Product; namespace App\Http\Requests\Admin\Master\Product;
use App\Concerns\CurrencyStripping;
use App\Enums\PriceType; use App\Enums\PriceType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class ProductVariantRequest extends FormRequest class ProductVariantRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
@ -15,14 +18,7 @@ public function authorize(): bool
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
$prices = $this->prices; $this->merge($this->stripCurrencyDot($this->all(), 'prices.*.price'));
if (is_array($prices)) {
foreach ($prices as $i => $price) {
if (isset($price['price']) && is_string($price['price'])) {
$this->request->set("prices.$i.price", (int) str_replace('.', '', $price['price']));
}
}
}
} }
public function rules(): array public function rules(): array

View File

@ -2,12 +2,15 @@
namespace App\Http\Requests\Admin\Master\RawMaterial; namespace App\Http\Requests\Admin\Master\RawMaterial;
use App\Concerns\CurrencyStripping;
use App\Enums\RawMaterialUnit; use App\Enums\RawMaterialUnit;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class RawMaterialRequest extends FormRequest class RawMaterialRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
@ -15,14 +18,7 @@ public function authorize(): bool
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
$variants = $this->variants; $this->merge($this->stripCurrencyDot($this->all(), 'variants.*.price'));
if (is_array($variants)) {
foreach ($variants as $i => $variant) {
if (isset($variant['price']) && is_string($variant['price'])) {
$this->request->set("variants.$i.price", (int) str_replace('.', '', $variant['price']));
}
}
}
} }
public function rules(): array public function rules(): array

View File

@ -2,10 +2,13 @@
namespace App\Http\Requests\Admin\Master\RawMaterial; namespace App\Http\Requests\Admin\Master\RawMaterial;
use App\Concerns\CurrencyStripping;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class RawMaterialVariantRequest extends FormRequest class RawMaterialVariantRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
@ -13,10 +16,7 @@ public function authorize(): bool
public function prepareForValidation(): void public function prepareForValidation(): void
{ {
$price = $this->price; $this->merge($this->stripCurrencyDot($this->all(), 'price'));
if (is_string($price)) {
$this->request->set('price', (int) str_replace('.', '', $price));
}
} }
public function rules(): array public function rules(): array

View File

@ -2,30 +2,23 @@
namespace App\Http\Requests\Admin\Settings; namespace App\Http\Requests\Admin\Settings;
use App\Concerns\CurrencyStripping;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Override; use Override;
class UpdateHRRequest extends FormRequest class UpdateHRRequest extends FormRequest
{ {
use CurrencyStripping;
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
#[Override] #[Override]
public function prepareForValidation() public function prepareForValidation(): void
{ {
if ($this->has('late_penalty_amount')) { $this->merge($this->stripCurrencyDot($this->all(), 'late_penalty_amount', 'absent_penalty_amount'));
$this->merge([
'late_penalty_amount' => str_replace('.', '', $this->late_penalty_amount),
]);
}
if ($this->has('absent_penalty_amount')) {
$this->merge([
'absent_penalty_amount' => str_replace('.', '', $this->absent_penalty_amount),
]);
}
} }
public function rules(): array public function rules(): array

View File

@ -10,6 +10,11 @@ class PasswordUpdateRequest extends FormRequest
{ {
use PasswordValidationRules; use PasswordValidationRules;
public function authorize(): bool
{
return true;
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -10,6 +10,11 @@ class ProfileDeleteRequest extends FormRequest
{ {
use PasswordValidationRules; use PasswordValidationRules;
public function authorize(): bool
{
return true;
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -8,6 +8,11 @@
class ProfileUpdateRequest extends FormRequest class ProfileUpdateRequest extends FormRequest
{ {
public function authorize(): bool
{
return true;
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -10,6 +10,11 @@ class TwoFactorAuthenticationRequest extends FormRequest
{ {
use InteractsWithTwoFactorState; use InteractsWithTwoFactorState;
public function authorize(): bool
{
return true;
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
* *

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -29,6 +30,13 @@ protected function casts(): array
]; ];
} }
protected function attendanceDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $value?->translatedFormat('l, d F Y'),
);
}
public function employee(): BelongsTo public function employee(): BelongsTo
{ {
return $this->belongsTo(Employee::class); return $this->belongsTo(Employee::class);

View File

@ -2,13 +2,16 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_balance', 'formatted_name'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class CashAccount extends Model class CashAccount extends Model
{ {
@ -21,6 +24,20 @@ protected function casts(): array
]; ];
} }
protected function formattedBalance(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->balance, 0, ',', '.'),
);
}
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
public function cashTransactions(): HasMany public function cashTransactions(): HasMany
{ {
return $this->hasMany(CashTransaction::class); return $this->hasMany(CashTransaction::class);

View File

@ -3,9 +3,11 @@
namespace App\Models; namespace App\Models;
use App\Enums\CashTransactionType; use App\Enums\CashTransactionType;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -16,6 +18,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[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
{ {
@ -30,6 +33,34 @@ protected function casts(): array
]; ];
} }
protected function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
);
}
protected function formattedBalanceAfter(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->balance_after, 0, ',', '.'),
);
}
protected function formattedCreatedAt(): Attribute
{
return Attribute::make(
get: fn () => $this->created_at?->translatedFormat('l, d F Y'),
);
}
protected function typeLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->type->label(),
);
}
#[Scope] #[Scope]
protected function deposit(Builder $query): void protected function deposit(Builder $query): void
{ {

View File

@ -2,19 +2,29 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Sluggable\Attributes\Sluggable; use Spatie\Sluggable\Attributes\Sluggable;
#[Appends(['formatted_name'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[Sluggable(from: 'name', to: 'slug')] #[Sluggable(from: 'name', to: 'slug')]
class Category extends Model class Category extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
public function products(): BelongsToMany public function products(): BelongsToMany
{ {
return $this->belongsToMany(Product::class, 'product_categories') return $this->belongsToMany(Product::class, 'product_categories')

View File

@ -2,17 +2,37 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_name', 'formatted_phone_number'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Customer extends Model class Customer extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
protected function formattedPhoneNumber(): Attribute
{
return Attribute::make(
get: fn () => $this->phone_number
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
: null,
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
);
}
public function orders(): HasMany public function orders(): HasMany
{ {
return $this->hasMany(Order::class); return $this->hasMany(Order::class);

View File

@ -3,9 +3,11 @@
namespace App\Models; namespace App\Models;
use App\Enums\CuttingStatus; use App\Enums\CuttingStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -14,6 +16,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_cost_per_unit', 'formatted_other_cost', 'formatted_sewing_cost', 'status_label', 'formatted_total_material_cost'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Cutting extends Model implements HasMedia class Cutting extends Model implements HasMedia
{ {
@ -30,6 +33,41 @@ protected function casts(): array
]; ];
} }
protected function formattedCostPerUnit(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->cost_per_unit, 0, ',', '.'),
);
}
protected function formattedOtherCost(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->other_cost, 0, ',', '.'),
);
}
protected function formattedSewingCost(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->sewing_cost, 0, ',', '.'),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
protected function formattedTotalMaterialCost(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total_material_cost, 0, ',', '.'),
);
}
#[Scope] #[Scope]
protected function cancelled(Builder $query): void protected function cancelled(Builder $query): void
{ {

View File

@ -3,15 +3,18 @@
namespace App\Models; namespace App\Models;
use App\Enums\EmploymentStatus; use App\Enums\EmploymentStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_base_salary', 'employment_status_label', 'formatted_join_date', 'formatted_resign_date'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Employee extends Model class Employee extends Model
{ {
@ -27,6 +30,34 @@ protected function casts(): array
]; ];
} }
protected function formattedBaseSalary(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->base_salary, 0, ',', '.'),
);
}
protected function employmentStatusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->employment_status->label(),
);
}
protected function formattedJoinDate(): Attribute
{
return Attribute::make(
get: fn () => $this->join_date?->translatedFormat('l, d F Y'),
);
}
protected function formattedResignDate(): Attribute
{
return Attribute::make(
get: fn () => $this->resign_date?->translatedFormat('l, d F Y'),
);
}
#[Scope] #[Scope]
protected function contract(Builder $query): void protected function contract(Builder $query): void
{ {

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\EmployeeAdvanceStatus; use App\Enums\EmployeeAdvanceStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
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_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class EmployeeAdvance extends Model class EmployeeAdvance extends Model
{ {
@ -28,6 +31,41 @@ protected function casts(): array
]; ];
} }
protected function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
);
}
protected function formattedCreatedAt(): Attribute
{
return Attribute::make(
get: fn () => $this->created_at?->translatedFormat('l, d F Y'),
);
}
protected function formattedDueDate(): Attribute
{
return Attribute::make(
get: fn () => $this->due_date?->translatedFormat('l, d F Y'),
);
}
protected function formattedPaidAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->paid_amount, 0, ',', '.'),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
#[Scope] #[Scope]
protected function approved(Builder $query): void protected function approved(Builder $query): void
{ {

View File

@ -2,7 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -10,6 +12,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_date'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Expense extends Model implements HasMedia class Expense extends Model implements HasMedia
{ {
@ -19,9 +22,31 @@ protected function casts(): array
{ {
return [ return [
'amount' => 'integer', 'amount' => 'integer',
'date' => 'date:Y-m-d',
]; ];
} }
protected function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
);
}
protected function formattedCreatedAt(): Attribute
{
return Attribute::make(
get: fn () => $this->created_at?->translatedFormat('l, d F Y'),
);
}
protected function formattedDate(): Attribute
{
return Attribute::make(
get: fn () => $this->date?->translatedFormat('l, d F Y'),
);
}
public function cashTransaction(): BelongsTo public function cashTransaction(): BelongsTo
{ {
return $this->belongsTo(CashTransaction::class); return $this->belongsTo(CashTransaction::class);

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\LeaveRequestStatus; use App\Enums\LeaveRequestStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_end_date', 'formatted_start_date', 'status_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class LeaveRequest extends Model class LeaveRequest extends Model
{ {
@ -27,6 +30,27 @@ protected function casts(): array
]; ];
} }
protected function formattedEndDate(): Attribute
{
return Attribute::make(
get: fn () => $this->end_date?->translatedFormat('l, d F Y'),
);
}
protected function formattedStartDate(): Attribute
{
return Attribute::make(
get: fn () => $this->start_date?->translatedFormat('l, d F Y'),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
#[Scope] #[Scope]
protected function approved(Builder $query): void protected function approved(Builder $query): void
{ {

View File

@ -6,9 +6,11 @@
use App\Enums\OrderStatus; use App\Enums\OrderStatus;
use App\Enums\PaymentType; use App\Enums\PaymentType;
use App\Enums\PriceType; use App\Enums\PriceType;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -17,6 +19,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['channel_label', 'formatted_cogs', 'formatted_discount', 'formatted_nego_price', 'payment_type_label', 'status_label', 'formatted_subtotal', 'formatted_total_amount'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Order extends Model implements HasMedia class Order extends Model implements HasMedia
{ {
@ -39,6 +42,62 @@ protected function casts(): array
]; ];
} }
protected function channelLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->channel->label(),
);
}
protected function formattedCogs(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->cogs, 0, ',', '.'),
);
}
protected function formattedDiscount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->discount, 0, ',', '.'),
);
}
protected function formattedNegoPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->nego_price, 0, ',', '.'),
);
}
protected function paymentTypeLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->payment_type->label(),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedTotalAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total_amount, 0, ',', '.'),
);
}
#[Scope] #[Scope]
protected function cancelled(Builder $query): void protected function cancelled(Builder $query): void
{ {

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProductStockQuality; use App\Enums\ProductStockQuality;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['stock_quality_label', 'formatted_subtotal', 'formatted_unit_price'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class OrderItem extends Model class OrderItem extends Model
{ {
@ -26,6 +29,27 @@ protected function casts(): array
]; ];
} }
protected function stockQualityLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->stock_quality->label(),
);
}
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedUnitPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
);
}
#[Scope] #[Scope]
protected function good(Builder $query): void protected function good(Builder $query): void
{ {

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Enums\PayrollStatus; use App\Enums\PayrollStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -13,6 +14,7 @@
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_base_salary', 'formatted_bonus_amount', 'formatted_deduction_amount', 'status_label', 'formatted_total_amount'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Payroll extends Model class Payroll extends Model
{ {
@ -30,10 +32,45 @@ protected function casts(): array
]; ];
} }
protected function formattedBaseSalary(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->base_salary, 0, ',', '.'),
);
}
protected function formattedBonusAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->bonus_amount, 0, ',', '.'),
);
}
protected function formattedDeductionAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->deduction_amount, 0, ',', '.'),
);
}
protected function formattedAmount(): Attribute protected function formattedAmount(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), get: fn () => 'Rp ' . number_format($this->attributes['total_amount'] ?? 0, 0, ',', '.'),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
protected function formattedTotalAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total_amount, 0, ',', '.'),
); );
} }

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\PayrollAdjustmentType; use App\Enums\PayrollAdjustmentType;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
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', 'type_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class PayrollAdjustment extends Model class PayrollAdjustment extends Model
{ {
@ -24,6 +27,20 @@ protected function casts(): array
]; ];
} }
protected function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
);
}
protected function typeLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->type->label(),
);
}
#[Scope] #[Scope]
protected function bonus(Builder $query): void protected function bonus(Builder $query): void
{ {

View File

@ -3,15 +3,18 @@
namespace App\Models; namespace App\Models;
use App\Enums\PayrollPeriodStatus; use App\Enums\PayrollPeriodStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['status_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class PayrollPeriod extends Model class PayrollPeriod extends Model
{ {
@ -27,6 +30,13 @@ protected function casts(): array
]; ];
} }
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
#[Scope] #[Scope]
protected function closed(Builder $query): void protected function closed(Builder $query): void
{ {

View File

@ -3,9 +3,11 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProductStatus; use App\Enums\ProductStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -13,6 +15,7 @@
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Sluggable\Attributes\Sluggable; use Spatie\Sluggable\Attributes\Sluggable;
#[Appends(['formatted_name', 'status_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[Sluggable(from: 'name', to: 'slug')] #[Sluggable(from: 'name', to: 'slug')]
class Product extends Model class Product extends Model
@ -26,6 +29,20 @@ protected function casts(): array
]; ];
} }
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
#[Scope] #[Scope]
protected function active(Builder $query): void protected function active(Builder $query): void
{ {

View File

@ -12,8 +12,8 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Appends(['formatted_price', 'type_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[Appends(['type_label'])]
class ProductPrice extends Model class ProductPrice extends Model
{ {
use HasFactory; use HasFactory;
@ -26,6 +26,13 @@ protected function casts(): array
]; ];
} }
protected function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->price, 0, ',', '.'),
);
}
#[Scope] #[Scope]
protected function retail(Builder $query): void protected function retail(Builder $query): void
{ {

View File

@ -2,7 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -11,6 +13,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_name'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class ProductVariant extends Model implements HasMedia class ProductVariant extends Model implements HasMedia
{ {
@ -25,6 +28,13 @@ protected function casts(): array
]; ];
} }
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
public function orderItems(): HasMany public function orderItems(): HasMany
{ {
return $this->hasMany(OrderItem::class); return $this->hasMany(OrderItem::class);

View File

@ -2,7 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -11,6 +13,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_discount', 'formatted_shipping_cost', 'formatted_subtotal', 'formatted_total'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Purchase extends Model implements HasMedia class Purchase extends Model implements HasMedia
{ {
@ -26,6 +29,34 @@ protected function casts(): array
]; ];
} }
protected function formattedDiscount(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->discount, 0, ',', '.'),
);
}
protected function formattedShippingCost(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->shipping_cost, 0, ',', '.'),
);
}
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedTotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total, 0, ',', '.'),
);
}
public function createdBy(): BelongsTo public function createdBy(): BelongsTo
{ {
return $this->belongsTo(User::class, 'created_by_id'); return $this->belongsTo(User::class, 'created_by_id');

View File

@ -2,12 +2,15 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_subtotal', 'formatted_unit_price'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class PurchaseItem extends Model class PurchaseItem extends Model
{ {
@ -22,6 +25,20 @@ protected function casts(): array
]; ];
} }
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedUnitPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
);
}
public function purchase(): BelongsTo public function purchase(): BelongsTo
{ {
return $this->belongsTo(Purchase::class); return $this->belongsTo(Purchase::class);

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\RawMaterialUnit; use App\Enums\RawMaterialUnit;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_name', 'unit_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class RawMaterial extends Model class RawMaterial extends Model
{ {
@ -24,6 +27,20 @@ protected function casts(): array
]; ];
} }
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
protected function unitLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->unit->label(),
);
}
#[Scope] #[Scope]
protected function kg(Builder $query): void protected function kg(Builder $query): void
{ {

View File

@ -3,7 +3,9 @@
namespace App\Models; namespace App\Models;
use App\Services\S3PresignedService; use App\Services\S3PresignedService;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -12,6 +14,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_price'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class RawMaterialPrice extends Model implements HasMedia class RawMaterialPrice extends Model implements HasMedia
{ {
@ -25,6 +28,13 @@ protected function casts(): array
]; ];
} }
protected function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->price, 0, ',', '.'),
);
}
public function cuttingMaterials(): HasMany public function cuttingMaterials(): HasMany
{ {
return $this->hasMany(CuttingMaterial::class); return $this->hasMany(CuttingMaterial::class);

View File

@ -3,9 +3,11 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProductStockQuality; use App\Enums\ProductStockQuality;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -14,6 +16,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['stock_type_label', 'formatted_subtotal', 'formatted_total'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Restock extends Model implements HasMedia class Restock extends Model implements HasMedia
{ {
@ -28,6 +31,27 @@ protected function casts(): array
]; ];
} }
protected function stockTypeLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->stock_type->label(),
);
}
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedTotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->total, 0, ',', '.'),
);
}
#[Scope] #[Scope]
protected function good(Builder $query): void protected function good(Builder $query): void
{ {

View File

@ -2,12 +2,15 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_subtotal', 'formatted_unit_price'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class RestockItem extends Model class RestockItem extends Model
{ {
@ -22,6 +25,20 @@ protected function casts(): array
]; ];
} }
protected function formattedSubtotal(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
);
}
protected function formattedUnitPrice(): Attribute
{
return Attribute::make(
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
);
}
public function productVariant(): BelongsTo public function productVariant(): BelongsTo
{ {
return $this->belongsTo(ProductVariant::class); return $this->belongsTo(ProductVariant::class);

View File

@ -3,15 +3,18 @@
namespace App\Models; namespace App\Models;
use App\Enums\StokOpnameStatus; use App\Enums\StokOpnameStatus;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_opname_date', 'status_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class StokOpname extends Model class StokOpname extends Model
{ {
@ -25,6 +28,20 @@ protected function casts(): array
]; ];
} }
protected function formattedOpnameDate(): Attribute
{
return Attribute::make(
get: fn () => $this->opname_date?->translatedFormat('l, d F Y'),
);
}
protected function statusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->status->label(),
);
}
#[Scope] #[Scope]
protected function cancelled(Builder $query): void protected function cancelled(Builder $query): void
{ {

View File

@ -3,13 +3,16 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProductStockQuality; use App\Enums\ProductStockQuality;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Appends(['stock_quality_label'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class StokOpnameItem extends Model class StokOpnameItem extends Model
{ {
@ -25,6 +28,13 @@ protected function casts(): array
]; ];
} }
protected function stockQualityLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->stock_quality->label(),
);
}
#[Scope] #[Scope]
protected function good(Builder $query): void protected function good(Builder $query): void
{ {

View File

@ -2,17 +2,37 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_name', 'formatted_phone_number'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class Supplier extends Model class Supplier extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
protected function formattedName(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->name),
);
}
protected function formattedPhoneNumber(): Attribute
{
return Attribute::make(
get: fn () => $this->phone_number
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
: null,
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
);
}
public function purchases(): HasMany public function purchases(): HasMany
{ {
return $this->hasMany(Purchase::class); return $this->hasMany(Purchase::class);

View File

@ -17,8 +17,8 @@
use NotificationChannels\WebPush\HasPushSubscriptions; use NotificationChannels\WebPush\HasPushSubscriptions;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
#[Appends(['full_name', 'name'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[Appends(['full_name'])]
class User extends Authenticatable class User extends Authenticatable
{ {
use HasFactory, HasPushSubscriptions, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable; use HasFactory, HasPushSubscriptions, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable;
@ -34,10 +34,11 @@ protected function casts(): array
]; ];
} }
#[Scope] protected function email(): Attribute
protected function active(Builder $query): void
{ {
$query->where('is_active', true); return Attribute::make(
set: fn (string $value) => strtolower($value),
);
} }
protected function name(): Attribute protected function name(): Attribute
@ -54,6 +55,12 @@ protected function fullName(): Attribute
); );
} }
#[Scope]
protected function active(Builder $query): void
{
$query->where('is_active', true);
}
public function attendances(): HasMany public function attendances(): HasMany
{ {
return $this->hasMany(Attendance::class); return $this->hasMany(Attendance::class);

View File

@ -3,14 +3,17 @@
namespace App\Models; namespace App\Models;
use App\Enums\Gender; use App\Enums\Gender;
use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[Appends(['formatted_birth_date', 'gender_label', 'formatted_phone_number'])]
#[Guarded(['id'])] #[Guarded(['id'])]
class UserProfile extends Model class UserProfile extends Model
{ {
@ -24,6 +27,30 @@ protected function casts(): array
]; ];
} }
protected function formattedBirthDate(): Attribute
{
return Attribute::make(
get: fn () => $this->birth_date?->translatedFormat('l, d F Y'),
);
}
protected function genderLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->gender?->label(),
);
}
protected function formattedPhoneNumber(): Attribute
{
return Attribute::make(
get: fn () => $this->phone_number
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
: null,
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
);
}
#[Scope] #[Scope]
protected function female(Builder $query): void protected function female(Builder $query): void
{ {

View File

@ -13,7 +13,7 @@
class AdminSettingsService class AdminSettingsService
{ {
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function getSystemData(): array public function getSystemData(): array

View File

@ -5,6 +5,7 @@
use App\Enums\CashTransactionType; use App\Enums\CashTransactionType;
use App\Models\CashAccount; use App\Models\CashAccount;
use App\Models\CashTransaction; use App\Models\CashTransaction;
use App\Services\Concerns\HandlesCashTransactions;
use App\Services\Concerns\RegistersMedia; use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService; use App\Services\NotificationService;
use App\Services\S3PresignedService; use App\Services\S3PresignedService;
@ -16,10 +17,10 @@
class CashAccountService class CashAccountService
{ {
use RegistersMedia; use HandlesCashTransactions, RegistersMedia;
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function get(): ?CashAccount public function get(): ?CashAccount
@ -94,27 +95,14 @@ private function formatTransaction(CashTransaction $transaction): array
public function deposit(array $data): CashTransaction public function deposit(array $data): CashTransaction
{ {
$transaction = DB::transaction(function () use ($data) { $transaction = DB::transaction(fn () => $this->creditCash(
$cashAccount = CashAccount::firstOrFail(); amount: $data['amount'],
$newBalance = $cashAccount->balance + $data['amount']; description: $data['description'],
));
$cashAccount->update(['balance' => $newBalance]); if (! empty($data['receipt_key'])) {
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
$transaction = CashTransaction::create([ }
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $data['amount'],
'balance_after' => $newBalance,
'type' => CashTransactionType::DEPOSIT,
'description' => $data['description'],
]);
if (! empty($data['receipt_key'])) {
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
}
return $transaction;
});
NotificationService::notify( NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
@ -128,33 +116,15 @@ public function deposit(array $data): CashTransaction
public function withdrawal(array $data): CashTransaction public function withdrawal(array $data): CashTransaction
{ {
$transaction = DB::transaction(function () use ($data) { $transaction = DB::transaction(fn () => $this->debitCash(
$cashAccount = CashAccount::firstOrFail(); amount: $data['amount'],
description: $data['description'],
type: CashTransactionType::WITHDRAWAL,
));
if ($cashAccount->balance < $data['amount']) { if (! empty($data['receipt_key'])) {
throw ValidationException::withMessages([ $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
'amount' => 'Saldo tidak mencukupi.', }
]);
}
$newBalance = $cashAccount->balance - $data['amount'];
$cashAccount->update(['balance' => $newBalance]);
$transaction = CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $data['amount'],
'balance_after' => $newBalance,
'type' => CashTransactionType::WITHDRAWAL,
'description' => $data['description'],
]);
if (! empty($data['receipt_key'])) {
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
}
return $transaction;
});
NotificationService::notify( NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],

View File

@ -7,6 +7,7 @@
use App\Models\CashAccount; use App\Models\CashAccount;
use App\Models\CashTransaction; use App\Models\CashTransaction;
use App\Models\EmployeeAdvance; use App\Models\EmployeeAdvance;
use App\Services\Concerns\HandlesCashTransactions;
use App\Services\NotificationService; use App\Services\NotificationService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -15,7 +16,8 @@
class EmployeeAdvanceService class EmployeeAdvanceService
{ {
public function getAll(): Collection use HandlesCashTransactions;
public function getAll(array $filters = []): Collection
{ {
return EmployeeAdvance::select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at') return EmployeeAdvance::select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
->with(['employee.user.userProfile']) ->with(['employee.user.userProfile'])
@ -23,7 +25,7 @@ public function getAll(): Collection
->get(); ->get();
} }
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{ {
return EmployeeAdvance::query() return EmployeeAdvance::query()
->select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at') ->select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
@ -36,30 +38,16 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
public function create(array $data): EmployeeAdvance public function create(array $data): EmployeeAdvance
{ {
$employeeAdvance = DB::transaction(function () use ($data) { $employeeAdvance = DB::transaction(function () use ($data) {
$cashAccount = CashAccount::firstOrFail();
$employee = auth()->user()->employee; $employee = auth()->user()->employee;
if (! $employee) { if (! $employee) {
throw new \Exception('Anda tidak terdaftar sebagai karyawan.'); throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
} }
if ($cashAccount->balance < $data['amount']) { $cashTransaction = $this->debitCash(
throw ValidationException::withMessages([ amount: $data['amount'],
'amount' => 'Saldo tidak mencukupi.', description: 'Kasbon: '.$data['description'],
]); );
}
$newBalance = $cashAccount->balance - $data['amount'];
$cashAccount->update(['balance' => $newBalance]);
$cashTransaction = CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $data['amount'],
'balance_after' => $newBalance,
'type' => CashTransactionType::EXPENSE,
'description' => 'Kasbon: '.$data['description'],
]);
return EmployeeAdvance::create([ return EmployeeAdvance::create([
'cash_transaction_id' => $cashTransaction->id, 'cash_transaction_id' => $cashTransaction->id,
@ -155,19 +143,10 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance
{ {
$employeeAdvance = DB::transaction(function () use ($employeeAdvance) { $employeeAdvance = DB::transaction(function () use ($employeeAdvance) {
$cashAccount = CashAccount::firstOrFail(); $cashTransaction = $this->creditCash(
amount: $employeeAdvance->amount,
$newBalance = $cashAccount->balance + $employeeAdvance->amount; description: 'Pembayaran kasbon: '.$employeeAdvance->description,
$cashAccount->update(['balance' => $newBalance]); );
$cashTransaction = CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $employeeAdvance->amount,
'balance_after' => $newBalance,
'type' => CashTransactionType::DEPOSIT,
'description' => 'Pembayaran kasbon: '.$employeeAdvance->description,
]);
$employeeAdvance->update([ $employeeAdvance->update([
'status' => EmployeeAdvanceStatus::PAID, 'status' => EmployeeAdvanceStatus::PAID,

View File

@ -6,6 +6,7 @@
use App\Models\CashAccount; use App\Models\CashAccount;
use App\Models\CashTransaction; use App\Models\CashTransaction;
use App\Models\Expense; use App\Models\Expense;
use App\Services\Concerns\HandlesCashTransactions;
use App\Services\Concerns\RegistersMedia; use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService; use App\Services\NotificationService;
use App\Services\S3PresignedService; use App\Services\S3PresignedService;
@ -17,13 +18,13 @@
class ExpenseService class ExpenseService
{ {
use RegistersMedia; use HandlesCashTransactions, RegistersMedia;
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function getAll(): Collection public function getAll(array $filters = []): Collection
{ {
return Expense::select('id', 'created_by_id', 'amount', 'description', 'created_at') return Expense::select('id', 'created_by_id', 'amount', 'description', 'created_at')
->with('createdBy.userProfile', 'media') ->with('createdBy.userProfile', 'media')
@ -32,7 +33,7 @@ public function getAll(): Collection
->map(fn (Expense $expense) => $this->formatExpense($expense)); ->map(fn (Expense $expense) => $this->formatExpense($expense));
} }
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{ {
$paginator = Expense::query() $paginator = Expense::query()
->select('id', 'created_by_id', 'amount', 'description', 'created_at') ->select('id', 'created_by_id', 'amount', 'description', 'created_at')
@ -78,25 +79,10 @@ private function formatExpense(Expense $expense): array
public function create(array $data): Expense public function create(array $data): Expense
{ {
$expense = DB::transaction(function () use ($data) { $expense = DB::transaction(function () use ($data) {
$cashAccount = CashAccount::firstOrFail(); $cashTransaction = $this->debitCash(
amount: $data['amount'],
if ($cashAccount->balance < $data['amount']) { description: $data['description'],
throw ValidationException::withMessages([ );
'amount' => 'Saldo tidak mencukupi.',
]);
}
$newBalance = $cashAccount->balance - $data['amount'];
$cashAccount->update(['balance' => $newBalance]);
$cashTransaction = CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $data['amount'],
'balance_after' => $newBalance,
'type' => CashTransactionType::EXPENSE,
'description' => $data['description'],
]);
$expense = Expense::create([ $expense = Expense::create([
'cash_transaction_id' => $cashTransaction->id, 'cash_transaction_id' => $cashTransaction->id,

View File

@ -9,6 +9,7 @@
use App\Models\CashTransaction; use App\Models\CashTransaction;
use App\Models\Payroll; use App\Models\Payroll;
use App\Models\PayrollPeriod; use App\Models\PayrollPeriod;
use App\Services\Concerns\HandlesCashTransactions;
use App\Services\NotificationService; use App\Services\NotificationService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -17,7 +18,8 @@
class PayrollPeriodService class PayrollPeriodService
{ {
public function getAll(): Collection use HandlesCashTransactions;
public function getAll(array $filters = []): Collection
{ {
return PayrollPeriod::select('id', 'year', 'month', 'status', 'closed_at', 'created_at') return PayrollPeriod::select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
->withCount('payrolls') ->withCount('payrolls')
@ -35,7 +37,7 @@ public function getAll(): Collection
->get(); ->get();
} }
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{ {
return PayrollPeriod::query() return PayrollPeriod::query()
->select('id', 'year', 'month', 'status', 'closed_at', 'created_at') ->select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
@ -123,19 +125,10 @@ public function pay(Payroll $payroll): Payroll
} }
$payroll = DB::transaction(function () use ($payroll) { $payroll = DB::transaction(function () use ($payroll) {
$cashAccount = CashAccount::firstOrFail(); $cashTransaction = $this->creditCash(
amount: $payroll->total_amount,
$newBalance = $cashAccount->balance + $payroll->total_amount; description: 'Pembayaran gaji karyawan',
$cashAccount->update(['balance' => $newBalance]); );
$cashTransaction = CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $payroll->total_amount,
'balance_after' => $newBalance,
'type' => CashTransactionType::DEPOSIT,
'description' => 'Pembayaran gaji karyawan',
]);
$payroll->update([ $payroll->update([
'status' => PayrollStatus::PAID, 'status' => PayrollStatus::PAID,

View File

@ -16,7 +16,7 @@ class AttendanceService
use RegistersMedia; use RegistersMedia;
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function getAll(): Collection public function getAll(): Collection

View File

@ -11,15 +11,16 @@ class EmployeeService
{ {
public function getAll(array $filters = []): Collection public function getAll(array $filters = []): Collection
{ {
return User::select('id', 'email', 'username', 'is_active') return User::select(['id', 'email', 'username', 'is_active'])
->whereHas('employee') ->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
->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)))
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender))) ->when($filters['gender'] ?? null, fn($q, $gender) => $q->whereHas('userProfile', fn($uq) => $uq->where('gender', $gender)))
->latest() ->latest()
->get(); ->get();
} }
@ -27,25 +28,21 @@ public function getAll(array $filters = []): Collection
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{ {
return User::query() return User::query()
->select('id', 'email', 'username', 'is_active') ->select(['id', 'email', 'username', 'is_active'])
->whereHas('employee') ->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
->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)))
->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)))
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender))) ->when($filters['gender'] ?? null, fn($q, $gender) => $q->whereHas('userProfile', fn($uq) => $uq->where('gender', $gender)))
->orderBy($sort, $direction) ->orderBy($sort, $direction)
->paginate($perPage); ->paginate($perPage);
} }
public function getById(int $id): User
{
return User::with(['userProfile', 'employee'])->findOrFail($id);
}
public function create(array $data): User public function create(array $data): User
{ {
return DB::transaction(function () use ($data) { return DB::transaction(function () use ($data) {
@ -56,6 +53,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 +63,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 +84,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 +94,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']);

View File

@ -19,7 +19,7 @@ class CuttingService
use RegistersMedia; use RegistersMedia;
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
@ -342,36 +342,12 @@ public function update(Cutting $cutting, array $data): Cutting
'updated_at' => $now, 'updated_at' => $now,
]); ]);
$this->syncCuttingPhoto($cutting, $data); $this->syncPhoto($cutting, $data);
return $cutting; return $cutting;
}); });
} }
private function syncCuttingPhoto(Cutting $cutting, array $data): void
{
if (! array_key_exists('photo_key', $data)) {
return;
}
$currentKey = $cutting->getFirstMedia('photos')?->file_name;
if ($data['photo_key'] === $currentKey) {
return;
}
$cutting->clearMediaCollection('photos');
if (! empty($data['photo_key'])) {
$this->registerMedia(
model: $cutting,
s3Key: $data['photo_key'],
collectionName: 'photos',
orderColumn: 1,
);
}
}
public function delete(Cutting $cutting): bool public function delete(Cutting $cutting): bool
{ {
return DB::transaction(function () use ($cutting) { return DB::transaction(function () use ($cutting) {

View File

@ -18,7 +18,7 @@ class PurchaseService
use RegistersMedia; use RegistersMedia;
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
@ -455,36 +455,12 @@ public function update(Purchase $purchase, array $data): Purchase
} }
DB::table('purchase_items')->insert($itemRows); DB::table('purchase_items')->insert($itemRows);
$this->syncPurchasePhoto($purchase, $data); $this->syncPhoto($purchase, $data);
return $purchase; return $purchase;
}); });
} }
private function syncPurchasePhoto(Purchase $purchase, array $data): void
{
if (! array_key_exists('photo_key', $data)) {
return;
}
$currentKey = $purchase->getFirstMedia('photos')?->file_name;
if ($data['photo_key'] === $currentKey) {
return;
}
$purchase->clearMediaCollection('photos');
if (! empty($data['photo_key'])) {
$this->registerMedia(
model: $purchase,
s3Key: $data['photo_key'],
collectionName: 'photos',
orderColumn: 1,
);
}
}
public function delete(Purchase $purchase): bool public function delete(Purchase $purchase): bool
{ {
return DB::transaction(function () use ($purchase) { return DB::transaction(function () use ($purchase) {

View File

@ -8,6 +8,7 @@
use App\Models\ProductVariant; use App\Models\ProductVariant;
use App\Models\Restock; use App\Models\Restock;
use App\Models\RestockItem; use App\Models\RestockItem;
use App\Services\Concerns\HasStockAdjustment;
use App\Services\Concerns\RegistersMedia; use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService; use App\Services\NotificationService;
use App\Services\S3PresignedService; use App\Services\S3PresignedService;
@ -16,15 +17,10 @@
class RestockService class RestockService
{ {
use RegistersMedia; use HasStockAdjustment, RegistersMedia;
private const QUALITY_STOCK_MAP = [
ProductStockQuality::GOOD->value => 'stock',
ProductStockQuality::REJECT->value => 'reject_stock',
];
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
@ -240,45 +236,4 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt
})->toArray(); })->toArray();
} }
private function applyStock(array $items, string $stockType, int $sign): void
{
foreach ($items as $item) {
$this->adjustVariantStock($item['product_variant_id'], $item['quantity'], $sign, $stockType);
}
}
private function adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void
{
$field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock';
if ($sign > 0) {
ProductVariant::whereKey($variantId)->increment($field, $quantity);
} else {
ProductVariant::whereKey($variantId)->decrement($field, $quantity);
}
}
private function syncPhoto(Restock $restock, array $data): void
{
if (! array_key_exists('photo_key', $data)) {
return;
}
$currentKey = $restock->getFirstMedia('photos')?->file_name;
if ($data['photo_key'] === $currentKey) {
return;
}
$restock->clearMediaCollection('photos');
if (! empty($data['photo_key'])) {
$this->registerMedia(
model: $restock,
s3Key: $data['photo_key'],
collectionName: 'photos',
orderColumn: 1,
);
}
}
} }

View File

@ -13,6 +13,7 @@
use App\Models\Product; use App\Models\Product;
use App\Models\ProductVariant; use App\Models\ProductVariant;
use App\Models\User; use App\Models\User;
use App\Services\Concerns\HasStockAdjustment;
use App\Services\Concerns\RegistersMedia; use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService; use App\Services\NotificationService;
use App\Services\S3PresignedService; use App\Services\S3PresignedService;
@ -21,12 +22,7 @@
class TransactionService class TransactionService
{ {
use RegistersMedia; use HasStockAdjustment, RegistersMedia;
private const QUALITY_STOCK_MAP = [
ProductStockQuality::GOOD->value => 'stock',
ProductStockQuality::REJECT->value => 'reject_stock',
];
private const SELLING_PRICE_MAP = [ private const SELLING_PRICE_MAP = [
PriceType::DISTRIBUTOR->value => PriceType::DISTRIBUTOR, PriceType::DISTRIBUTOR->value => PriceType::DISTRIBUTOR,
@ -39,7 +35,7 @@ class TransactionService
]; ];
public function __construct( public function __construct(
private S3PresignedService $s3Service = new S3PresignedService, private readonly S3PresignedService $s3Service,
) {} ) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
@ -96,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')
@ -149,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(),
]; ];
} }
@ -378,48 +377,6 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp
})->toArray(); })->toArray();
} }
private function applyStock(array $items, string $stockType, int $sign): void
{
foreach ($items as $item) {
$this->adjustVariantStock($item['product_variant_id'], $item['quantity'], $sign, $stockType);
}
}
private function adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void
{
$field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock';
if ($sign > 0) {
ProductVariant::whereKey($variantId)->increment($field, $quantity);
} else {
ProductVariant::whereKey($variantId)->decrement($field, $quantity);
}
}
private function syncPhoto(Order $order, array $data): void
{
if (! array_key_exists('photo_key', $data)) {
return;
}
$currentKey = $order->getFirstMedia('photos')?->file_name;
if ($data['photo_key'] === $currentKey) {
return;
}
$order->clearMediaCollection('photos');
if (! empty($data['photo_key'])) {
$this->registerMedia(
model: $order,
s3Key: $data['photo_key'],
collectionName: 'photos',
orderColumn: 1,
);
}
}
private function generateOrderNumber(): string private function generateOrderNumber(): string
{ {
$prefix = 'TRX'; $prefix = 'TRX';

Some files were not shown because too many files have changed in this diff Show More