Compare commits
34 Commits
bc02af2a66
...
485ae8e5ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
485ae8e5ce | ||
|
|
0cce4ee73d | ||
|
|
3fab97e7e5 | ||
|
|
1918f35ab2 | ||
|
|
2bb814702a | ||
|
|
a1ba9564c7 | ||
|
|
eba21d3d3a | ||
|
|
0023309a8f | ||
|
|
46af27e537 | ||
|
|
1c19e6c690 | ||
|
|
48c031a86f | ||
|
|
6627422655 | ||
|
|
f7c13826eb | ||
|
|
515b2e9848 | ||
|
|
f7ce2f1620 | ||
|
|
cd0995f04e | ||
|
|
4733df5827 | ||
|
|
3c945a1372 | ||
|
|
4b70da6715 | ||
|
|
14140c39cc | ||
|
|
e4d956843c | ||
|
|
166419134f | ||
|
|
d648d48632 | ||
|
|
28f7ea0006 | ||
|
|
d10f731839 | ||
|
|
85556f8779 | ||
|
|
f7a91739f9 | ||
|
|
add157183b | ||
|
|
9e60a1cf39 | ||
|
|
74c374e126 | ||
|
|
50b2be68ac | ||
|
|
de45ae3bb7 | ||
|
|
c6a87a64c9 | ||
|
|
7a1b107ce5 |
88
.ai/CONTEXT.md
Normal file
88
.ai/CONTEXT.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# DST Collection — Project Context
|
||||||
|
|
||||||
|
## Tentang Project
|
||||||
|
**DST Collection** adalah aplikasi ERP untuk brand pakaian wanita (daster, dress, dll). Aplikasi ini mengelola dua area bisnis utama:
|
||||||
|
|
||||||
|
1. **Toko** — penjualan, produk, keuangan, HR
|
||||||
|
2. **Konveksi** — bahan baku, cutting, pembelian bahan
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|------------|
|
||||||
|
| Backend | Laravel 13, PHP 8.3 |
|
||||||
|
| Frontend | React 19, Inertia.js 3, TypeScript |
|
||||||
|
| UI | shadcn/ui, Radix UI, Tailwind CSS 4 |
|
||||||
|
| Auth | Laravel Fortify, Spatie Permission |
|
||||||
|
| DB | MySQL (SoftDeletes di semua model utama) |
|
||||||
|
|
||||||
|
## Roles & Akses
|
||||||
|
|
||||||
|
### role: `developer`, `owner`
|
||||||
|
- **Akses**: Semua module, semua aksi
|
||||||
|
- **Catatan**: Selalu muncul di semua sidebar
|
||||||
|
|
||||||
|
### role: `admin-toko`
|
||||||
|
- **Akses**: Full CRUD semua module toko
|
||||||
|
- **Modules**: Kategori, Produk, Customer, Transaksi, Restock, Kas Toko, Pengeluaran, Kasbon, Gaji, Pegawai, Presensi, Cuti, Settings
|
||||||
|
- **Catatan**: TIDAK akses Supplier, Bahan Baku, Belanja, Cutting
|
||||||
|
|
||||||
|
### role: `direktur`
|
||||||
|
- **Akses**: Readonly semua data toko + bisa ajukan kasbon
|
||||||
|
- **Modules**: Semua module toko (readonly), kecuali Supplier, Bahan Baku, Belanja, Cutting
|
||||||
|
- **Catatan**: Bisa lihat semua data pegawai toko, tapi bukan admin bahan baku
|
||||||
|
|
||||||
|
### role: `admin-bahan-baku`
|
||||||
|
- **Akses**: Full CRUD untuk konveksi
|
||||||
|
- **Modules**: Bahan Baku, Supplier, Belanja, Cutting, Pegawai (hanya role admin-bahan-baku), Kasbon (lihat semua + ACC/bayar)
|
||||||
|
- **Catatan**: TIDAK akses module toko lainnya
|
||||||
|
|
||||||
|
### role: `cashier`
|
||||||
|
- **Akses**: Transaksi (full CRUD), Customer (full CRUD), Produk (readonly), Kasbon (CRUD own data)
|
||||||
|
- **Modules**: Transaksi, Customer, Produk (view), Kasbon, Gaji (view), Presensi, Cuti
|
||||||
|
|
||||||
|
### role: `marketing-offline`, `marketing-online`
|
||||||
|
- **Akses**: Transaksi (readonly yang punya dia), Produk (readonly), Customer (full CRUD)
|
||||||
|
- **Modules**: Transaksi (view only own), Produk (view), Customer, Kasbon (CRUD own data), Presensi, Cuti
|
||||||
|
|
||||||
|
### role: `stok-opname`
|
||||||
|
- **Akses**: Stok Opname (full CRUD + submit)
|
||||||
|
- **Modules**: Stok Opname, Produk (view), Presensi, Cuti
|
||||||
|
|
||||||
|
### role: `non-operator`
|
||||||
|
- **Akses**: Basic (dashboard, presensi, cuti, kasbon)
|
||||||
|
- **Modules**: Presensi, Cuti, Kasbon (CRUD own data)
|
||||||
|
|
||||||
|
## Module Overview
|
||||||
|
|
||||||
|
| # | Module | Route Group | Controller Path | Service Path |
|
||||||
|
|---|--------|-------------|-----------------|--------------|
|
||||||
|
| 1 | Kategori | `admin/master/categories` | `Admin/Master/CategoryController` | `Admin/Master/CategoryService` |
|
||||||
|
| 2 | Produk | `admin/master/products` | `Admin/Master/Product/ProductController` | `Admin/Master/Product/ProductService` |
|
||||||
|
| 3 | Bahan Baku | `admin/master/raw-materials` | `Admin/Master/RawMaterial/RawMaterialController` | `Admin/Master/RawMaterial/RawMaterialService` |
|
||||||
|
| 4 | Supplier | `admin/master/suppliers` | `Admin/Master/SupplierController` | `Admin/Master/SupplierService` |
|
||||||
|
| 5 | Customer | `admin/master/customers` | `Admin/Master/CustomerController` | `Admin/Master/CustomerService` |
|
||||||
|
| 6 | Belanja | `admin/manage/purchases` | `Admin/Manage/PurchaseController` | `Admin/Manage/PurchaseService` |
|
||||||
|
| 7 | Cutting | `admin/manage/cuttings` | `Admin/Manage/CuttingController` | `Admin/Manage/CuttingService` |
|
||||||
|
| 8 | Transaksi | `admin/manage/transactions` | `Admin/Manage/TransactionController` | `Admin/Manage/TransactionService` |
|
||||||
|
| 9 | Restock | `admin/manage/restocks` | `Admin/Manage/RestockController` | `Admin/Manage/RestockService` |
|
||||||
|
| 10 | Stok Opname | `admin/manage/stok-opnames` | (via StockMutationController) | — |
|
||||||
|
| 11 | Kas Toko | `admin/finance/cash-accounts` | `Admin/Finance/CashAccountController` | `Admin/Finance/Cash/CashAccountService`, `Admin/Finance/Cash/CashTransactionService` |
|
||||||
|
| 12 | Pengeluaran | `admin/finance/expenses` | `Admin/Finance/ExpenseController` | `Admin/Finance/ExpenseService` |
|
||||||
|
| 13 | Kasbon | `admin/finance/employee-advances` | `Admin/Finance/EmployeeAdvanceController` | `Admin/Finance/EmployeeAdvanceService` |
|
||||||
|
| 14 | Gaji | `admin/finance/payroll-periods` | `Admin/Finance/PayrollController` | `Admin/Finance/Payroll/PayrollPeriodService`, `Admin/Finance/Payroll/PayrollAdjustmentService` |
|
||||||
|
| 15 | Pegawai | `admin/hr/employees` | `Admin/HR/EmployeeController` | `Admin/HR/EmployeeService` |
|
||||||
|
| 16 | Presensi | `admin/hr/attendances` | `Admin/HR/AttendanceController` | `Admin/HR/AttendanceService` |
|
||||||
|
| 17 | Cuti | `admin/hr/leave-requests` | `Admin/HR/LeaveRequestController` | `Admin/HR/LeaveRequestService` |
|
||||||
|
| 18 | Role & Permission | `admin/settings/roles` | `Admin/RoleController` | `Admin/Settings/RoleService` |
|
||||||
|
| 19 | Pengaturan | `admin/settings` | `Admin/AdminSettingsController` | `Admin/AdminSettingsService` |
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
| Metric | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| Tables | 48 |
|
||||||
|
| Models | 43 |
|
||||||
|
| Enums | 21 |
|
||||||
|
| Services | 30 |
|
||||||
|
| Controllers | 32 |
|
||||||
|
| Roles | 9 |
|
||||||
|
| Permissions | ~120 |
|
||||||
1712
.ai/CONVENTIONS.md
Normal file
1712
.ai/CONVENTIONS.md
Normal file
File diff suppressed because it is too large
Load Diff
400
.ai/REFERENCE.md
Normal file
400
.ai/REFERENCE.md
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
# Database Reference
|
||||||
|
|
||||||
|
> Format: `table_name` → `model_name` | columns | relationships
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth & User
|
||||||
|
|
||||||
|
### `users` → User
|
||||||
|
`id` `email`(unique) `username`(unique) `password` `is_active`(bool) `last_login_at`(datetime) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: email_verified_at(datetime), is_active(bool), last_login_at(datetime), password(hashed), two_factor_confirmed_at(datetime)
|
||||||
|
- Scopes: active()
|
||||||
|
- Relations: userProfile(HasOne→UserProfile), employee(HasOne→Employee), attendances(HasMany→Attendance), cashAccounts(HasMany→CashAccount,created_by_id), cashTransactions(HasMany→CashTransaction,created_by_id), createdCuttings(HasMany→Cutting,created_by_id), submittedCuttings(HasMany→Cutting,submitted_by_id), createdExpenses(HasMany→Expense,created_by_id), createdOrders(HasMany→Order,created_by_id), marketingOrders(HasMany→Order,marketing_id), orderItems(HasMany→OrderItem), createdPurchases(HasMany→Purchase,created_by_id), createdRestocks(HasMany→Restock,created_by_id), stokOpnamesCreated(HasMany→StokOpname,created_by_id), stokOpnamesVerified(HasMany→StokOpname,verified_by_id), employeeAdvancesPaid(HasMany→EmployeeAdvance,paid_by_id), employeeAdvancesVerified(HasMany→EmployeeAdvance,verified_by_id), paidPayrolls(HasMany→Payroll,paid_by_id), payrollPeriodsClosed(HasMany→PayrollPeriod,closed_by_id), notifications(HasMany→AppNotification), rejections(HasMany→Rejection,rejected_by_id), pushSubscriptions(HasMany→PushSubscription,morph)
|
||||||
|
|
||||||
|
### `user_profiles` → UserProfile
|
||||||
|
`id` `user_id`(FK→users,unique) `full_name`(200) `phone_number`(20,null) `gender`(enum,null) `birth_date`(date,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: gender(Gender), birth_date(date:Y-m-d)
|
||||||
|
- Scopes: female(), male(), hasPhoneNumber()
|
||||||
|
- Relations: user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `push_subscriptions` → PushSubscription
|
||||||
|
`id` `subscribable_type` `subscribable_id` `endpoint`(500,unique) `public_key`(null) `auth_token`(null) `content_encoding`(null) `created_at` `updated_at`
|
||||||
|
- Relations: user(MorphTo)
|
||||||
|
|
||||||
|
### `notifications` → AppNotification
|
||||||
|
`id` `user_id`(FK→users) `title` `body`(text,null) `url`(null) `is_read`(bool,default:false) `read_at`(datetime,null) `created_at` `updated_at`
|
||||||
|
- Casts: is_read(bool), read_at(datetime)
|
||||||
|
- Accessor: formatted_read_at → 'l, d F Y H:i'
|
||||||
|
- Relations: user(BelongsTo→User)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Master Data
|
||||||
|
|
||||||
|
### `categories` → Category
|
||||||
|
`id` `name`(50) `slug`(50,unique) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Relations: products(BelongsToMany→Product via product_categories)
|
||||||
|
- Accessor: formattedName → ucfirst(name)
|
||||||
|
|
||||||
|
### `product_categories` → Category (Pivot)
|
||||||
|
`product_id`(FK→products) `category_id`(FK→categories)
|
||||||
|
- No id, no timestamps
|
||||||
|
- Relations: category(BelongsTo→Category), product(BelongsTo→Product)
|
||||||
|
|
||||||
|
### `products` → Product
|
||||||
|
`id` `name`(200) `slug`(200,unique) `description`(text,null) `status`(enum,default:active) `rejection_reason`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(ProductStatus)
|
||||||
|
- Scopes: active(), draft(), inactive(), pending(), rejected()
|
||||||
|
- Relations: categories(BelongsToMany→Category), productVariants(HasMany→ProductVariant)
|
||||||
|
|
||||||
|
### `product_variants` → ProductVariant
|
||||||
|
`id` `product_id`(FK→products) `name`(200) `stock`(uint,default:0) `reject_stock`(uint,default:0) `retail_stock`(uint,default:0) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: stock(int), reject_stock(int), retail_stock(int)
|
||||||
|
- GlobalScope: orderBy(name)
|
||||||
|
- Relations: product(BelongsTo→Product), productPrices(HasMany→ProductPrice,variant_id), orderItems(HasMany→OrderItem), restockItems(HasMany→RestockItem), retailStockHistories(HasMany→RetailStockHistory), stokOpnameItems(HasMany→StokOpnameItem), stockMutations(HasMany→StockMutation,morph)
|
||||||
|
|
||||||
|
### `product_prices` → ProductPrice
|
||||||
|
`id` `variant_id`(FK→product_variants) `type`(enum) `price`(uint) `created_at` `updated_at`
|
||||||
|
- UNIQUE(variant_id, type)
|
||||||
|
- Casts: type(PriceType), price(int)
|
||||||
|
- Scopes: retail(), wholesale()
|
||||||
|
- Relations: variant(BelongsTo→ProductVariant)
|
||||||
|
|
||||||
|
### `customers` → Customer
|
||||||
|
`id` `name`(200) `phone_number`(20,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Relations: orders(HasMany→Order)
|
||||||
|
- Accessor: formattedPhoneNumber (get: "0821 2121 2121", set: strip whitespace)
|
||||||
|
|
||||||
|
### `suppliers` → Supplier
|
||||||
|
`id` `name`(200) `phone_number`(20,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Relations: purchases(HasMany→Purchase)
|
||||||
|
- Accessor: formattedPhoneNumber (get: "0821 2121 2121", set: strip whitespace)
|
||||||
|
|
||||||
|
### `raw_materials` → RawMaterial
|
||||||
|
`id` `name`(200) `unit`(enum) `is_active`(bool,default:true) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: unit(RawMaterialUnit), is_active(bool)
|
||||||
|
- Scopes: active(), nonactive(), kg(), meter(), yard()
|
||||||
|
- Relations: rawMaterialPrices(HasMany→RawMaterialPrice)
|
||||||
|
|
||||||
|
### `raw_material_prices` → RawMaterialPrice
|
||||||
|
`id` `raw_material_id`(FK→raw_materials) `variant`(200) `price`(uint) `stock`(uint,default:0) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: price(int), stock(int)
|
||||||
|
- GlobalScope: orderBy(variant)
|
||||||
|
- Relations: rawMaterial(BelongsTo→RawMaterial,withTrashed), cuttingMaterials(HasMany→CuttingMaterial), purchaseItems(HasMany→PurchaseItem)
|
||||||
|
- Accessor: photo_url → first media presigned S3 URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finance
|
||||||
|
|
||||||
|
### `cash_accounts` → CashAccount
|
||||||
|
`id` `created_by_id`(FK→users) `name`(200) `balance`(ubig,default:0) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: balance(int)
|
||||||
|
- Relations: cashTransactions(HasMany→CashTransaction), createdBy(BelongsTo→User)
|
||||||
|
|
||||||
|
### `cash_transactions` → CashTransaction
|
||||||
|
`id` `cash_account_id`(FK→cash_accounts) `created_by_id`(FK→users) `reference_id`(ubig,null,morph) `reference_type`(string,null,morph) `amount`(ubig) `balance_after`(ubig) `type`(enum,default:deposit) `description`(100) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: type(CashTransactionType), amount(int), balance_after(int)
|
||||||
|
- Scopes: deposit(), expenseType(), transfer(), withdrawal()
|
||||||
|
- Relations: cashAccount(BelongsTo→CashAccount), createdBy(BelongsTo→User), reference(MorphTo), expense(HasOne→Expense), order(HasOne→Order), employeeAdvances(HasMany→EmployeeAdvance), payrolls(HasMany→Payroll)
|
||||||
|
|
||||||
|
### `expenses` → Expense
|
||||||
|
`id` `cash_transaction_id`(FK→cash_transactions,unique,null) `created_by_id`(FK→users) `amount`(ubig) `description`(100) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: amount(int), date(date:Y-m-d)
|
||||||
|
- Relations: cashTransaction(BelongsTo→CashTransaction), createdBy(BelongsTo→User)
|
||||||
|
|
||||||
|
### `employee_advances` → EmployeeAdvance
|
||||||
|
`id` `paid_by_id`(FK→users,null) `employee_id`(FK→employees) `verified_by_id`(FK→users,null) `repayment_cash_transaction_id`(FK→cash_transactions,unique,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `amount`(ubig) `paid_amount`(ubig,default:0) `description`(100) `due_date`(date) `status`(enum,default:pending) `verified_at`(datetime,null) `paid_at`(datetime,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(EmployeeAdvanceStatus), amount(int), paid_amount(int), due_date(date:Y-m-d), verified_at(datetime), paid_at(datetime)
|
||||||
|
- Scopes: approved(), cancelled(), paid(), pending(), rejected()
|
||||||
|
- Relations: employee(BelongsTo→Employee), cashTransaction(BelongsTo→CashTransaction), repaymentCashTransaction(BelongsTo→CashTransaction), paidBy(BelongsTo→User), verifiedBy(BelongsTo→User), payments(HasMany→EmployeeAdvancePayment)
|
||||||
|
|
||||||
|
### `employee_advance_payments` → EmployeeAdvancePayment
|
||||||
|
`id` `employee_advance_id`(FK→employee_advances) `paid_by_id`(FK→users,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `amount`(ubig) `description`(100,null) `paid_at`(datetime) `created_at` `updated_at`
|
||||||
|
- No soft deletes
|
||||||
|
- Casts: amount(int), paid_at(datetime)
|
||||||
|
- Accessor: formatted_amount → 'Rp X.XXX', formatted_paid_at → 'l, d F Y H:i'
|
||||||
|
- Relations: employeeAdvance(BelongsTo→EmployeeAdvance), paidBy(BelongsTo→User), cashTransaction(BelongsTo→CashTransaction)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HR
|
||||||
|
|
||||||
|
### `employees` → Employee
|
||||||
|
`id` `user_id`(FK→users,unique) `join_date`(date) `resign_date`(date,null) `employment_status`(enum,default:full_time) `base_salary`(uint) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: employment_status(EmploymentStatus), base_salary(int), join_date(date:Y-m-d), resign_date(date:Y-m-d)
|
||||||
|
- Scopes: contract(), fullTime(), internship(), partTime(), resigned()
|
||||||
|
- Relations: user(BelongsTo→User), attendances(HasMany→Attendance), employeeAdvances(HasMany→EmployeeAdvance), leaveRequests(HasMany→LeaveRequest), payrolls(HasMany→Payroll)
|
||||||
|
|
||||||
|
### `attendances` → Attendance
|
||||||
|
`id` `employee_id`(FK→employees) `attendance_date`(date) `check_in_at`(datetime) `check_out_at`(datetime,null) `check_in_latitude`(decimal(10,7),null) `check_in_longitude`(decimal(10,7),null) `check_out_latitude`(decimal(10,7),null) `check_out_longitude`(decimal(10,7),null) `work_duration_minutes`(uint,null) `created_at` `updated_at`
|
||||||
|
- No soft deletes
|
||||||
|
- Casts: attendance_date(date:Y-m-d), check_in_at(datetime), check_out_at(datetime), check_in/out_latitude/longitude(decimal:7), work_duration_minutes(int)
|
||||||
|
- Accessor: formatted_attendance_date → 'l, d F Y', formatted_check_in_at → 'l, d F Y H:i', formatted_check_out_at → 'l, d F Y H:i'
|
||||||
|
- Relations: employee(BelongsTo→Employee), payrollAdjustments(HasMany→PayrollAdjustment)
|
||||||
|
|
||||||
|
### `payroll_periods` → PayrollPeriod
|
||||||
|
`id` `closed_by_id`(FK→users,null) `year`(usmall) `month`(utiny) `status`(enum,default:open) `closed_at`(datetime,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(PayrollPeriodStatus), year(int), month(int), closed_at(datetime)
|
||||||
|
- Accessor: formatted_closed_at → 'l, d F Y H:i', month_name → 'Januari', status_label
|
||||||
|
- Scopes: closed(), open()
|
||||||
|
- Relations: closedBy(BelongsTo→User), payrolls(HasMany→Payroll)
|
||||||
|
|
||||||
|
### `payrolls` → Payroll
|
||||||
|
`id` `payroll_period_id`(FK→payroll_periods) `employee_id`(FK→employees) `cash_transaction_id`(FK→cash_transactions,unique,null) `paid_by_id`(FK→users,null) `base_salary`(uint) `bonus_amount`(ubig,default:0) `deduction_amount`(ubig,default:0) `total_amount`(ubig) `status`(enum,default:unpaid) `paid_at`(datetime,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(PayrollStatus), base_salary(int), bonus_amount(int), deduction_amount(int), total_amount(int), paid_at(datetime)
|
||||||
|
- Scopes: cancelled(), paid(), unpaid()
|
||||||
|
- Relations: payrollPeriod(BelongsTo→PayrollPeriod), employee(BelongsTo→Employee), cashTransaction(BelongsTo→CashTransaction), paidBy(BelongsTo→User), payrollAdjustments(HasMany→PayrollAdjustment)
|
||||||
|
|
||||||
|
### `payroll_adjustments` → PayrollAdjustment
|
||||||
|
`id` `payroll_id`(FK→payrolls) `attendance_id`(FK→attendances,null) `created_by_id`(FK→users) `type`(enum) `amount`(ubig) `description`(100) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: type(PayrollAdjustmentType), amount(int)
|
||||||
|
- Scopes: bonus(), deduction()
|
||||||
|
- Relations: payroll(BelongsTo→Payroll), attendance(BelongsTo→Attendance), createdBy(BelongsTo→User)
|
||||||
|
|
||||||
|
### `leave_requests` → LeaveRequest
|
||||||
|
`id` `employee_id`(FK→employees) `verified_by_id`(FK→users,null) `start_date`(date) `end_date`(date) `total_days`(uint) `status`(enum,default:pending) `verified_at`(datetime,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(LeaveRequestStatus), start_date(date:Y-m-d), end_date(date:Y-m-d), total_days(int), verified_at(datetime)
|
||||||
|
- Scopes: approved(), cancelled(), pending(), rejected()
|
||||||
|
- Relations: employee(BelongsTo→Employee), verifiedBy(BelongsTo→User)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales
|
||||||
|
|
||||||
|
### `orders` → Order
|
||||||
|
`id` `customer_id`(FK→customers,null) `marketing_id`(FK→users,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `created_by_id`(FK→users) `order_number`(30,unique) `channel`(enum) `price_type`(enum) `status`(enum,default:pending) `payment_type`(enum,default:cash) `is_affiliate`(bool,default:false) `tiktok_order_id`(100,null) `shopee_order_id`(100,null) `subtotal`(ubig) `discount`(ubig,default:0) `nego_price`(ubig,null) `marketplace_settings_snapshot`(json,null) `total_amount`(ubig) `cogs`(ubig,default:0) `notes`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: channel(OrderChannel), price_type(PriceType), status(OrderStatus), payment_type(PaymentType), is_affiliate(bool), subtotal(int), discount(int), nego_price(int), total_amount(int), cogs(int), marketplace_settings_snapshot(array)
|
||||||
|
- Scopes: cancelled(), cash(), completed(), pending(), processing(), qris(), refunded(), retail(), shopee(), store(), tiktok(), transfer(), wholesale()
|
||||||
|
- Relations: cashTransaction(BelongsTo→CashTransaction), createdBy(BelongsTo→User), customer(BelongsTo→Customer), marketing(BelongsTo→User), orderItems(HasMany→OrderItem)
|
||||||
|
|
||||||
|
### `order_items` → OrderItem
|
||||||
|
`id` `order_id`(FK→orders,null) `user_id`(FK→users,null) `product_variant_id`(FK→product_variants) `stock_quality`(enum,default:good) `quantity`(uint) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: stock_quality(ProductStockQuality), quantity(int), unit_price(int), subtotal(int)
|
||||||
|
- Scopes: good(), reject()
|
||||||
|
- Relations: order(BelongsTo→Order), productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `rejections` → Rejection
|
||||||
|
`id` `rejectable_type`(string,null,morph) `rejectable_id`(ubig,null,morph) `rejected_by_id`(FK→users) `reason`(500) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Relations: rejectable(MorphTo), rejectedBy(BelongsTo→User)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
### `cuttings` → Cutting
|
||||||
|
`id` `created_by_id`(FK→users) `submitted_by_id`(FK→users,null) `status`(enum,default:in_progress) `description`(100,null) `total_material_cost`(ubig,null) `sewing_cost`(ubig,default:0) `other_cost`(ubig,default:0) `cost_per_unit`(ubig,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(CuttingStatus), total_material_cost(int), cost_per_unit(int), sewing_cost(int), other_cost(int)
|
||||||
|
- Scopes: cancelled(), completed(), inProgress()
|
||||||
|
- Relations: createdBy(BelongsTo→User), submittedBy(BelongsTo→User), cuttingMaterialCombinations(HasMany→CuttingMaterialCombination), cuttingMaterials(HasMany→CuttingMaterial), cuttingResults(HasMany→CuttingResult)
|
||||||
|
|
||||||
|
### `cutting_material_combinations` → CuttingMaterialCombination
|
||||||
|
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `material_result`(int,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: material_result(int)
|
||||||
|
- Accessor: formatted_material_result → 'X.XXX'
|
||||||
|
- Relations: cutting(BelongsTo→Cutting), cuttingMaterials(HasMany→CuttingMaterial,combination_id), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `cutting_materials` → CuttingMaterial
|
||||||
|
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `raw_material_price_id`(FK→raw_material_prices) `combination_id`(FK→cutting_material_combinations,null) `material_usage`(int) `material_result`(int,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: material_usage(int), material_result(int)
|
||||||
|
- Accessor: formatted_material_result → 'X.XXX', formatted_material_usage → 'X.XXX'
|
||||||
|
- Relations: cutting(BelongsTo→Cutting), rawMaterialPrice(BelongsTo→RawMaterialPrice), combination(BelongsTo→CuttingMaterialCombination), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `cutting_results` → CuttingResult
|
||||||
|
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `product_name`(255,null) `cutting_result`(uint,null) `sample`(uint,null) `original_outside_sample`(uint,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: cutting_result(int), sample(int), original_outside_sample(int)
|
||||||
|
- Accessor: formatted_cutting_result → 'X.XXX', formatted_original_outside_sample → 'X.XXX', formatted_sample → 'X.XXX'
|
||||||
|
- Relations: cutting(BelongsTo→Cutting), user(BelongsTo→User)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
### `purchases` → Purchase
|
||||||
|
`id` `supplier_id`(FK→suppliers) `created_by_id`(FK→users) `subtotal`(ubig) `discount`(ubig,default:0) `shipping_cost`(ubig,default:0) `total`(ubig) `notes`(100,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: subtotal(int), discount(int), shipping_cost(int), total(int)
|
||||||
|
- Relations: createdBy(BelongsTo→User), supplier(BelongsTo→Supplier), purchaseItems(HasMany→PurchaseItem)
|
||||||
|
|
||||||
|
### `purchase_items` → PurchaseItem
|
||||||
|
`id` `purchase_id`(FK→purchases,null) `user_id`(FK→users,null) `raw_material_price_id`(FK→raw_material_prices) `quantity`(uint) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: quantity(int), unit_price(int), subtotal(int)
|
||||||
|
- Relations: purchase(BelongsTo→Purchase), rawMaterialPrice(BelongsTo→RawMaterialPrice,withTrashed), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `restocks` → Restock
|
||||||
|
`id` `created_by_id`(FK→users) `subtotal`(ubig) `total`(ubig) `notes`(100,null) `stock_type`(enum,default:good) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: stock_type(ProductStockQuality), subtotal(int), total(int)
|
||||||
|
- Scopes: good(), reject()
|
||||||
|
- Relations: createdBy(BelongsTo→User), restockItems(HasMany→RestockItem)
|
||||||
|
|
||||||
|
### `restock_items` → RestockItem
|
||||||
|
`id` `restock_id`(FK→restocks,null) `user_id`(FK→users,null) `product_variant_id`(FK→product_variants) `quantity`(int) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: quantity(int), unit_price(int), subtotal(int)
|
||||||
|
- Relations: restock(BelongsTo→Restock), productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `stok_opnames` → StokOpname
|
||||||
|
`id` `created_by_id`(FK→users) `verified_by_id`(FK→users,null) `opname_date`(date) `status`(enum,default:draft) `notes`(text,null) `verification_notes`(text,null) `created_at` `updated_at` `deleted_at`
|
||||||
|
- Casts: status(StokOpnameStatus), opname_date(date:Y-m-d)
|
||||||
|
- Scopes: cancelled(), completed(), draft(), inProgress(), verified()
|
||||||
|
- Relations: createdBy(BelongsTo→User), verifiedBy(BelongsTo→User), stokOpnameItems(HasMany→StokOpnameItem)
|
||||||
|
|
||||||
|
### `stok_opname_items` → StokOpnameItem
|
||||||
|
`id` `stok_opname_id`(FK→stok_opnames) `product_variant_id`(FK→product_variants) `stock_quality`(enum,default:good) `system_stock`(uint,default:0) `physical_stock`(uint,default:0) `difference`(int,default:0) `notes`(text,null) `created_at` `updated_at`
|
||||||
|
- No soft deletes
|
||||||
|
- Casts: stock_quality(ProductStockQuality), system_stock(int), physical_stock(int), difference(int)
|
||||||
|
- Scopes: good(), reject()
|
||||||
|
- Relations: stokOpname(BelongsTo→StokOpname), productVariant(BelongsTo→ProductVariant)
|
||||||
|
|
||||||
|
### `retail_stock_histories` → RetailStockHistory
|
||||||
|
`id` `product_variant_id`(FK→product_variants) `user_id`(FK→users) `quantity`(uint) `stock_before`(uint) `retail_stock_before`(uint) `stock_after`(uint) `retail_stock_after`(uint) `notes`(255,null) `created_at`
|
||||||
|
- No soft deletes, no updated_at
|
||||||
|
- $timestamps = false
|
||||||
|
- Casts: quantity(int), stock_before(int), retail_stock_before(int), stock_after(int), retail_stock_after(int)
|
||||||
|
- Accessor: formatted_quantity → 'X.XXX', formatted_retail_stock_before → 'X.XXX', formatted_retail_stock_after → 'X.XXX', formatted_stock_before → 'X.XXX', formatted_stock_after → 'X.XXX'
|
||||||
|
- Relations: productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)
|
||||||
|
|
||||||
|
### `stock_mutations` → StockMutation
|
||||||
|
`id` `user_id`(FK→users) `stockable_type`(string) `stockable_id`(ubig) `type`(string) `source_type`(string,null) `source_id`(ubig,null) `quantity`(int) `stock_before`(int) `stock_after`(int) `stock_quality`(string,null) `description`(string,null) `created_at` `updated_at`
|
||||||
|
- No soft deletes
|
||||||
|
- Casts: quantity(int), stock_before(int), stock_after(int)
|
||||||
|
- Accessor: formatted_quantity → 'X.XXX', formatted_stock_after → 'X.XXX', formatted_stock_before → 'X.XXX'
|
||||||
|
- Relations: stockable(MorphTo), source(MorphTo), user(BelongsTo→User)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
### `system_configurations` → SystemConfiguration
|
||||||
|
`id` `created_at` `updated_at`
|
||||||
|
- Singleton table
|
||||||
|
|
||||||
|
### `homepage_configurations` → HomepageConfiguration
|
||||||
|
`id` `created_at` `updated_at`
|
||||||
|
- Singleton table
|
||||||
|
|
||||||
|
|
||||||
|
### `settings` (laravel-settings)
|
||||||
|
`id` `group`(string) `name`(string) `locked`(bool,default:false) `payload`(json) `created_at` `updated_at`
|
||||||
|
- UNIQUE(group, name)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
| Enum | Values | Used In |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `CashTransactionType` | deposit, expense, transfer, withdrawal | cash_transactions.type |
|
||||||
|
| `CuttingStatus` | in_progress, completed, cancelled | cuttings.status |
|
||||||
|
| `EmployeeAdvanceStatus` | pending, approved, rejected, paid, cancelled | employee_advances.status |
|
||||||
|
| `EmploymentStatus` | full_time, part_time, contract, internship, resigned | employees.employment_status |
|
||||||
|
| `Gender` | male, female | user_profiles.gender |
|
||||||
|
| `LeaveRequestStatus` | pending, approved, rejected, cancelled | leave_requests.status |
|
||||||
|
| `Modules` | — | Permission module names |
|
||||||
|
| `OrderChannel` | store, shopee, tiktok | orders.channel |
|
||||||
|
| `OrderStatus` | pending, processing, completed, cancelled, refunded | orders.status |
|
||||||
|
| `PaymentType` | cash, transfer, qris | orders.payment_type |
|
||||||
|
| `PayrollAdjustmentType` | bonus, deduction | payroll_adjustments.type |
|
||||||
|
| `PayrollPeriodStatus` | open, closed | payroll_periods.status |
|
||||||
|
| `PayrollStatus` | unpaid, paid, cancelled | payrolls.status |
|
||||||
|
| `Permission` | — | Permission action names |
|
||||||
|
| `PriceType` | retail, wholesale, capital | product_prices.type, orders.price_type |
|
||||||
|
| `ProductStatus` | active, draft, inactive, pending, rejected | products.status |
|
||||||
|
| `ProductStockQuality` | good, reject | order_items.stock_quality, restocks.stock_type, stok_opname_items.stock_quality |
|
||||||
|
| `RawMaterialUnit` | kg, meter, yard | raw_materials.unit |
|
||||||
|
| `Role` | — | Role names |
|
||||||
|
| `StokOpnameStatus` | draft, in_progress, completed, verified, cancelled | stok_opnames.status |
|
||||||
|
|
||||||
|
## Service Concerns
|
||||||
|
|
||||||
|
| Trait | Methods | Used By |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `HandlesCashTransactions` | getCashAccount(), creditCash(), debitCash() | CashAccountService, ExpenseService, EmployeeAdvanceService, PayrollPeriodService |
|
||||||
|
| `HasStockAdjustment` | adjustStock(), adjustVariantStock(), applyStock(), reverseStock() | TransactionService, RestockService |
|
||||||
|
| `RegistersMedia` | registerMedia(), syncPhoto() | CashAccountService, CuttingService, ExpenseService, PurchaseService, RestockService, TransactionService |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Form Request Validation Rules
|
||||||
|
|
||||||
|
> Validasi harus SESUAI dengan DB schema (type + max length).
|
||||||
|
|
||||||
|
### Master
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `CategoryRequest` | `name` | `required, string, max:50, unique:categories,name` | ✅ varchar(50) |
|
||||||
|
| `CustomerRequest` | `name` | `required, string, max:200, unique:customers,name` | ✅ varchar(200) |
|
||||||
|
| `CustomerRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
|
||||||
|
| `CustomerRequest` | `address` | `nullable, string` | ✅ text |
|
||||||
|
| `SupplierRequest` | `name` | `required, string, max:200, unique:suppliers,name` | ✅ varchar(200) |
|
||||||
|
| `SupplierRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
|
||||||
|
| `RawMaterialRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |
|
||||||
|
| `RawMaterialRequest` | `unit` | `required, in:kg,meter,yard` | ✅ enum |
|
||||||
|
| `RawMaterialVariantRequest` | `variant` | `required, string, max:200` | ✅ varchar(200) |
|
||||||
|
| `RawMaterialVariantRequest` | `price` | `required, integer, min:0` | ✅ uint |
|
||||||
|
| `ProductRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |
|
||||||
|
| `ProductVariantRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |
|
||||||
|
|
||||||
|
### Finance
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `CashAccountRequest` | `name` | `required, string, max:200, unique:cash_accounts,name` | ✅ varchar(200) |
|
||||||
|
| `CashTransactionRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
|
||||||
|
| `CashTransactionRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `ExpenseRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
|
||||||
|
| `ExpenseRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `EmployeeAdvanceRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
|
||||||
|
| `EmployeeAdvanceRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `EmployeeAdvanceRequest` | `due_date` | `required, date, after_or_equal:today` | ✅ date |
|
||||||
|
| `EmployeeAdvancePaymentRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
|
||||||
|
| `EmployeeAdvancePaymentRequest` | `description` | `nullable, string, max:100` | ✅ varchar(100) nullable |
|
||||||
|
| `PayrollAdjustmentRequest` | `type` | `required, in:bonus,deduction` | ✅ enum |
|
||||||
|
| `PayrollAdjustmentRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
|
||||||
|
| `PayrollAdjustmentRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
|
||||||
|
|
||||||
|
### HR
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `EmployeeRequest` | `email` | `required, email, max:100, unique:users,email` | ✅ varchar(100) |
|
||||||
|
| `EmployeeRequest` | `username` | `required, string, max:20, alpha_dash, unique:users,username` | ✅ varchar(20) |
|
||||||
|
| `EmployeeRequest` | `full_name` | `required, string, max:200` | ✅ varchar(200) |
|
||||||
|
| `EmployeeRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
|
||||||
|
| `EmployeeRequest` | `gender` | `nullable, in:male,female` | ✅ enum |
|
||||||
|
| `EmployeeRequest` | `base_salary` | `required, integer, min:0` | ✅ uint |
|
||||||
|
| `AttendanceRequest` | `latitude` | `required, numeric, min:-90, max:90` | ✅ decimal(10,7) |
|
||||||
|
| `AttendanceRequest` | `longitude` | `required, numeric, min:-180, max:180` | ✅ decimal(10,7) |
|
||||||
|
| `LeaveRequestRequest` | `start_date` | `required, date, after:today` | ✅ date |
|
||||||
|
| `LeaveRequestRequest` | `end_date` | `required, date, after_or_equal:start_date` | ✅ date |
|
||||||
|
|
||||||
|
### Sales
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `TransactionRequest` | `channel` | `required, in:store,shopee,tiktok` | ✅ enum |
|
||||||
|
| `TransactionRequest` | `price_type` | `required, in:retail,wholesale` | ✅ enum |
|
||||||
|
| `TransactionRequest` | `payment_type` | `required, in:cash,transfer,qris` | ✅ enum |
|
||||||
|
| `TransactionRequest` | `tiktok_order_id` | `nullable, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `TransactionRequest` | `shopee_order_id` | `nullable, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `TransactionRequest` | `notes` | `nullable, string, max:100` | ✅ text (UI limit) |
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `CuttingRequest` | `description` | `nullable, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `CuttingRequest` | `product_name` | `required, string, max:255` | ✅ varchar(255) |
|
||||||
|
| `CuttingRequest` | `cutting_result` | `required, integer, min:1` | ✅ uint |
|
||||||
|
| `CuttingRequest` | `sample` | `required, integer` | ✅ uint |
|
||||||
|
| `CuttingRequest` | `original_outside_sample` | `required, integer` | ✅ uint |
|
||||||
|
|
||||||
|
### Inventory
|
||||||
|
|
||||||
|
| Request | Field | Rules | DB Match |
|
||||||
|
|---------|-------|-------|----------|
|
||||||
|
| `PurchaseRequest` | `supplier_id` | `required, integer, exists:suppliers,id` | ✅ FK |
|
||||||
|
| `PurchaseRequest` | `notes` | `nullable, string, max:100` | ✅ varchar(100) |
|
||||||
|
| `RestockRequest` | `stock_type` | `required, in:good,reject` | ✅ enum |
|
||||||
|
| `RestockRequest` | `notes` | `nullable, string, max:100` | ✅ varchar(100) |
|
||||||
|
|
||||||
15
app/Concerns/HasRoleChecks.php
Normal file
15
app/Concerns/HasRoleChecks.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Concerns;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
|
|
||||||
|
trait HasRoleChecks
|
||||||
|
{
|
||||||
|
public static function hasAnyRole(array $roles): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->hasAnyRole(
|
||||||
|
array_map(fn (Role $role) => $role->value, $roles)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -121,11 +121,6 @@ enum Permission: string
|
|||||||
case PAYROLL_CANCEL = 'payroll.cancel';
|
case PAYROLL_CANCEL = 'payroll.cancel';
|
||||||
case PAYROLL_ADJUST = 'payroll.adjust';
|
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
|
// Restocks
|
||||||
case RESTOCKS_VIEW = 'restocks.view';
|
case RESTOCKS_VIEW = 'restocks.view';
|
||||||
case RESTOCKS_CREATE = 'restocks.create';
|
case RESTOCKS_CREATE = 'restocks.create';
|
||||||
|
|||||||
@ -11,6 +11,8 @@ enum ProductStatus: string
|
|||||||
case ACTIVE = 'active';
|
case ACTIVE = 'active';
|
||||||
case INACTIVE = 'inactive';
|
case INACTIVE = 'inactive';
|
||||||
case DRAFT = 'draft';
|
case DRAFT = 'draft';
|
||||||
|
case PENDING = 'pending';
|
||||||
|
case REJECTED = 'rejected';
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
@ -18,6 +20,8 @@ public function label(): string
|
|||||||
self::ACTIVE => 'Aktif',
|
self::ACTIVE => 'Aktif',
|
||||||
self::INACTIVE => 'Non Aktif',
|
self::INACTIVE => 'Non Aktif',
|
||||||
self::DRAFT => 'Draft',
|
self::DRAFT => 'Draft',
|
||||||
|
self::PENDING => 'Menunggu Verifikasi',
|
||||||
|
self::REJECTED => 'Ditolak',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,14 @@ enum ProductStockQuality: string
|
|||||||
|
|
||||||
case GOOD = 'good';
|
case GOOD = 'good';
|
||||||
case REJECT = 'reject';
|
case REJECT = 'reject';
|
||||||
|
case RETAIL = 'retail';
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::GOOD => 'Bagus',
|
self::GOOD => 'Bagus',
|
||||||
self::REJECT => 'Reject',
|
self::REJECT => 'Reject',
|
||||||
|
self::RETAIL => 'Ecer',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use App\Traits\ProvidesEnumOptions;
|
|
||||||
|
|
||||||
enum Role: string
|
enum Role: string
|
||||||
{
|
{
|
||||||
use ProvidesEnumOptions;
|
|
||||||
|
|
||||||
case DEVELOPER = 'developer';
|
case DEVELOPER = 'developer';
|
||||||
case OWNER = 'owner';
|
case OWNER = 'owner';
|
||||||
case ADMIN_TOKO = 'admin-toko';
|
case ADMIN_TOKO = 'admin-toko';
|
||||||
@ -95,7 +91,6 @@ public function permissions(): array
|
|||||||
Permission::CUSTOMERS_VIEW,
|
Permission::CUSTOMERS_VIEW,
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
Permission::PRODUCTS_VIEW,
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
Permission::ORDERS_VIEW,
|
||||||
|
|
||||||
@ -139,8 +134,6 @@ public function permissions(): array
|
|||||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||||
Permission::ANALYSIS_MARKETING_SALES,
|
Permission::ANALYSIS_MARKETING_SALES,
|
||||||
|
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::STOK_OPNAMES_VIEW,
|
Permission::STOK_OPNAMES_VIEW,
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
Permission::ATTENDANCES_VIEW,
|
||||||
@ -167,9 +160,6 @@ public function permissions(): array
|
|||||||
Permission::PRODUCTS_UPDATE,
|
Permission::PRODUCTS_UPDATE,
|
||||||
Permission::PRODUCTS_DELETE,
|
Permission::PRODUCTS_DELETE,
|
||||||
Permission::PRODUCTS_TOGGLE_STATUS,
|
Permission::PRODUCTS_TOGGLE_STATUS,
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::OWNER_VERIFICATIONS_VIEW,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
Permission::ORDERS_VIEW,
|
||||||
Permission::ORDERS_CREATE,
|
Permission::ORDERS_CREATE,
|
||||||
@ -295,9 +285,6 @@ public function permissions(): array
|
|||||||
Permission::RAW_MATERIALS_CREATE,
|
Permission::RAW_MATERIALS_CREATE,
|
||||||
Permission::RAW_MATERIALS_UPDATE,
|
Permission::RAW_MATERIALS_UPDATE,
|
||||||
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::OWNER_VERIFICATIONS_VIEW,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
Permission::PRODUCTS_VIEW,
|
||||||
Permission::PRODUCTS_CREATE,
|
Permission::PRODUCTS_CREATE,
|
||||||
@ -397,7 +384,6 @@ public function permissions(): array
|
|||||||
Permission::LEAVE_REQUESTS_DELETE,
|
Permission::LEAVE_REQUESTS_DELETE,
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
Permission::PRODUCTS_VIEW,
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::STOK_OPNAMES_VIEW,
|
Permission::STOK_OPNAMES_VIEW,
|
||||||
Permission::STOK_OPNAMES_CREATE,
|
Permission::STOK_OPNAMES_CREATE,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ public function updateSystem(UpdateSystemRequest $request): RedirectResponse
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan sistem berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan sistem berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.settings.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateHomepage(UpdateHomepageRequest $request): RedirectResponse
|
public function updateHomepage(UpdateHomepageRequest $request): RedirectResponse
|
||||||
@ -45,7 +45,7 @@ public function updateHomepage(UpdateHomepageRequest $request): RedirectResponse
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan homepage berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan homepage berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.settings.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateSocialMedia(UpdateSocialMediaRequest $request): RedirectResponse
|
public function updateSocialMedia(UpdateSocialMediaRequest $request): RedirectResponse
|
||||||
@ -54,7 +54,7 @@ public function updateSocialMedia(UpdateSocialMediaRequest $request): RedirectRe
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan media sosial berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan media sosial berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.settings.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateMarketplace(UpdateMarketplaceRequest $request): RedirectResponse
|
public function updateMarketplace(UpdateMarketplaceRequest $request): RedirectResponse
|
||||||
@ -63,7 +63,7 @@ public function updateMarketplace(UpdateMarketplaceRequest $request): RedirectRe
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan marketplace berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengaturan marketplace berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.settings.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateHR(UpdateHRRequest $request): RedirectResponse
|
public function updateHR(UpdateHRRequest $request): RedirectResponse
|
||||||
@ -72,6 +72,6 @@ public function updateHR(UpdateHRRequest $request): RedirectResponse
|
|||||||
|
|
||||||
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 back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\CashTransaction;
|
use App\Models\CashTransaction;
|
||||||
use App\Services\Admin\Finance\CashAccountService;
|
use App\Services\Admin\Finance\Cash\CashAccountService;
|
||||||
|
use App\Services\Admin\Finance\Cash\CashTransactionService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -15,16 +16,15 @@
|
|||||||
class CashAccountController extends Controller
|
class CashAccountController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CashAccountService $service
|
private CashAccountService $cashAccountService,
|
||||||
|
private CashTransactionService $cashTransactionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
$cashAccount = $this->service->get();
|
|
||||||
|
|
||||||
return Inertia::render('admin/finance/cash-account/index', [
|
return Inertia::render('admin/finance/cash-account/index', [
|
||||||
'cashAccount' => $cashAccount,
|
'cashAccount' => $this->cashAccountService->get(),
|
||||||
'transactions' => $this->service->paginatedTransactions(
|
'transactions' => $this->cashTransactionService->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
filters: $request->only(['type']),
|
filters: $request->only(['type']),
|
||||||
),
|
),
|
||||||
@ -38,7 +38,7 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function deposit(CashTransactionRequest $request): RedirectResponse
|
public function deposit(CashTransactionRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->deposit($request->validated()),
|
fn () => $this->cashTransactionService->deposit($request->validated()),
|
||||||
'Deposit berhasil ditambahkan.',
|
'Deposit berhasil ditambahkan.',
|
||||||
'admin.finance.cash-accounts.index'
|
'admin.finance.cash-accounts.index'
|
||||||
);
|
);
|
||||||
@ -47,7 +47,7 @@ public function deposit(CashTransactionRequest $request): RedirectResponse
|
|||||||
public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->withdrawal($request->validated()),
|
fn () => $this->cashTransactionService->withdrawal($request->validated()),
|
||||||
'Withdrawal berhasil ditambahkan.',
|
'Withdrawal berhasil ditambahkan.',
|
||||||
'admin.finance.cash-accounts.index'
|
'admin.finance.cash-accounts.index'
|
||||||
);
|
);
|
||||||
@ -56,7 +56,7 @@ public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
|||||||
public function update(CashTransactionRequest $request, CashTransaction $transaction): RedirectResponse
|
public function update(CashTransactionRequest $request, CashTransaction $transaction): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->updateTransaction($transaction, $request->validated()),
|
fn () => $this->cashTransactionService->update($transaction, $request->validated()),
|
||||||
'Transaksi berhasil diperbarui.',
|
'Transaksi berhasil diperbarui.',
|
||||||
'admin.finance.cash-accounts.index'
|
'admin.finance.cash-accounts.index'
|
||||||
);
|
);
|
||||||
@ -65,7 +65,7 @@ public function update(CashTransactionRequest $request, CashTransaction $transac
|
|||||||
public function destroy(CashTransaction $transaction): RedirectResponse
|
public function destroy(CashTransaction $transaction): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->deleteTransaction($transaction),
|
fn () => $this->cashTransactionService->destroy($transaction),
|
||||||
'Transaksi berhasil dihapus.',
|
'Transaksi berhasil dihapus.',
|
||||||
'admin.finance.cash-accounts.index'
|
'admin.finance.cash-accounts.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,7 +36,7 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function store(EmployeeAdvanceRequest $request): RedirectResponse
|
public function store(EmployeeAdvanceRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Kasbon berhasil ditambahkan.',
|
'Kasbon berhasil ditambahkan.',
|
||||||
'admin.finance.employee-advances.index'
|
'admin.finance.employee-advances.index'
|
||||||
);
|
);
|
||||||
@ -54,7 +54,7 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe
|
|||||||
public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
|
public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($employeeAdvance),
|
fn () => $this->service->destroy($employeeAdvance),
|
||||||
'Kasbon berhasil dihapus.',
|
'Kasbon berhasil dihapus.',
|
||||||
'admin.finance.employee-advances.index'
|
'admin.finance.employee-advances.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function store(ExpenseRequest $request): RedirectResponse
|
public function store(ExpenseRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Pengeluaran berhasil ditambahkan.',
|
'Pengeluaran berhasil ditambahkan.',
|
||||||
'admin.finance.expenses.index'
|
'admin.finance.expenses.index'
|
||||||
);
|
);
|
||||||
@ -45,7 +45,7 @@ public function update(ExpenseRequest $request, Expense $expense): RedirectRespo
|
|||||||
public function destroy(Expense $expense): RedirectResponse
|
public function destroy(Expense $expense): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($expense),
|
fn () => $this->service->destroy($expense),
|
||||||
'Pengeluaran berhasil dihapus.',
|
'Pengeluaran berhasil dihapus.',
|
||||||
'admin.finance.expenses.index'
|
'admin.finance.expenses.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance\Payroll;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest;
|
use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Models\PayrollAdjustment;
|
use App\Models\PayrollAdjustment;
|
||||||
use App\Services\Admin\Finance\PayrollAdjustmentService;
|
use App\Services\Admin\Finance\Payroll\PayrollAdjustmentService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
class PayrollAdjustmentController extends Controller
|
class PayrollAdjustmentController extends Controller
|
||||||
@ -18,7 +18,7 @@ public function __construct(
|
|||||||
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($payroll, $request->validated()),
|
fn () => $this->service->store($payroll, $request->validated()),
|
||||||
'Adjustment gaji berhasil ditambahkan.',
|
'Adjustment gaji berhasil ditambahkan.',
|
||||||
'admin.finance.payroll-periods.show',
|
'admin.finance.payroll-periods.show',
|
||||||
parameters: ['payroll_period' => $payroll->payroll_period_id]
|
parameters: ['payroll_period' => $payroll->payroll_period_id]
|
||||||
@ -28,7 +28,7 @@ public function store(PayrollAdjustmentRequest $request, Payroll $payroll): Redi
|
|||||||
public function destroy(PayrollAdjustment $payrollAdjustment): RedirectResponse
|
public function destroy(PayrollAdjustment $payrollAdjustment): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($payrollAdjustment),
|
fn () => $this->service->destroy($payrollAdjustment),
|
||||||
'Adjustment gaji berhasil dihapus.',
|
'Adjustment gaji berhasil dihapus.',
|
||||||
'admin.finance.payroll-periods.show',
|
'admin.finance.payroll-periods.show',
|
||||||
parameters: ['payroll_period' => $payrollAdjustment->payroll->payroll_period_id]
|
parameters: ['payroll_period' => $payrollAdjustment->payroll->payroll_period_id]
|
||||||
@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance\Payroll;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Services\Admin\Finance\PayrollPeriodService;
|
use App\Services\Admin\Finance\Payroll\PayrollPeriodService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
class PayrollController extends Controller
|
class PayrollController extends Controller
|
||||||
@ -1,18 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance\Payroll;
|
||||||
|
|
||||||
use App\Enums\PayrollPeriodStatus;
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\PayrollPeriod;
|
use App\Models\PayrollPeriod;
|
||||||
use App\Services\Admin\Finance\PayrollPeriodService;
|
use App\Services\Admin\Finance\Payroll\PayrollPeriodService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
class PayrollPeriodController extends Controller
|
class PayrollPeriodController extends Controller
|
||||||
{
|
{
|
||||||
|
use HasRoleChecks;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private PayrollPeriodService $service
|
private PayrollPeriodService $service
|
||||||
) {}
|
) {}
|
||||||
@ -26,22 +29,23 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function current(): RedirectResponse
|
public function current(): RedirectResponse
|
||||||
{
|
{
|
||||||
$now = now();
|
$period = $this->service->getCurrentOrCreate();
|
||||||
|
|
||||||
$period = PayrollPeriod::firstOrCreate(
|
|
||||||
['year' => $now->year, 'month' => $now->month],
|
|
||||||
['status' => PayrollPeriodStatus::OPEN]
|
|
||||||
);
|
|
||||||
|
|
||||||
return to_route('admin.finance.payroll-periods.show', ['payroll_period' => $period->id]);
|
return to_route('admin.finance.payroll-periods.show', ['payroll_period' => $period->id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(PayrollPeriod $payrollPeriod): Response
|
public function show(PayrollPeriod $payrollPeriod): Response
|
||||||
{
|
{
|
||||||
$period = $this->service->getDetail($payrollPeriod);
|
$payrollPeriod->load([
|
||||||
|
'payrolls' => function ($query) {
|
||||||
|
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
|
||||||
|
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||||
|
->orderBy('id');
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return Inertia::render('admin/finance/payroll-period/show', [
|
return Inertia::render('admin/finance/payroll-period/show', [
|
||||||
'payrollPeriod' => $period,
|
'payrollPeriod' => $payrollPeriod,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6,7 +6,6 @@
|
|||||||
use App\Http\Requests\Admin\HR\AttendanceRequest;
|
use App\Http\Requests\Admin\HR\AttendanceRequest;
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Services\Admin\HR\AttendanceService;
|
use App\Services\Admin\HR\AttendanceService;
|
||||||
use App\Settings\HRSettings;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -22,38 +21,9 @@ public function index(Request $request): Response
|
|||||||
{
|
{
|
||||||
$year = $request->integer('year', now()->year);
|
$year = $request->integer('year', now()->year);
|
||||||
$month = $request->integer('month', now()->month);
|
$month = $request->integer('month', now()->month);
|
||||||
$hrSettings = app(HRSettings::class);
|
|
||||||
$user = auth()->user();
|
|
||||||
$isAdmin = $user->hasAnyRole(['developer', 'owner', 'direktur']);
|
|
||||||
|
|
||||||
if ($isAdmin) {
|
|
||||||
return Inertia::render('admin/hr/attendance/index', [
|
|
||||||
'attendances' => $this->service->getByMonth($year, $month),
|
|
||||||
'todayAttendance' => null,
|
|
||||||
'currentYear' => $year,
|
|
||||||
'currentMonth' => $month,
|
|
||||||
'monthStats' => $this->service->getMonthStats($year, $month),
|
|
||||||
'hrSettings' => [
|
|
||||||
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
|
||||||
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
|
||||||
],
|
|
||||||
'isAdmin' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$employeeId = $user->employee?->id;
|
|
||||||
|
|
||||||
return Inertia::render('admin/hr/attendance/index', [
|
return Inertia::render('admin/hr/attendance/index', [
|
||||||
'attendances' => $this->service->getByMonth($year, $month, $employeeId),
|
...$this->service->getIndexData($year, $month),
|
||||||
'todayAttendance' => $this->service->getToday(),
|
|
||||||
'currentYear' => $year,
|
|
||||||
'currentMonth' => $month,
|
|
||||||
'monthStats' => $this->service->getMonthStats($year, $month, $employeeId),
|
|
||||||
'hrSettings' => [
|
|
||||||
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
|
||||||
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
|
||||||
],
|
|
||||||
'isAdmin' => false,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,20 +2,25 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\HR;
|
namespace App\Http\Controllers\Admin\HR;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\HR\EmployeeRequest;
|
use App\Http\Requests\Admin\HR\EmployeeRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Admin\HR\EmployeeService;
|
use App\Services\Admin\HR\EmployeeService;
|
||||||
|
use App\Services\Admin\Settings\RoleService;
|
||||||
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
|
||||||
{
|
{
|
||||||
|
use HasRoleChecks;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EmployeeService $service
|
private EmployeeService $service,
|
||||||
|
private RoleService $roleService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -26,28 +31,22 @@ public function index(PaginatedRequest $request): Response
|
|||||||
filters: $request->only(['employment_status', 'is_active', 'gender']),
|
filters: $request->only(['employment_status', 'is_active', 'gender']),
|
||||||
),
|
),
|
||||||
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
|
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
'canViewAll' => self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
return Inertia::render('admin/hr/employee/create', [
|
return Inertia::render('admin/hr/employee/create', [
|
||||||
'roles' => $this->service->canViewAll()
|
'roles' => $this->roleService->getForEmployee(),
|
||||||
? Role::where('name', '!=', 'Developer')
|
'canViewAll' => self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]),
|
||||||
->when($this->service->shouldHideAdminBahanBaku(), fn ($q) => $q->where('name', '!=', 'admin-bahan-baku'))
|
|
||||||
->get(['id', 'name'])
|
|
||||||
: Role::where('name', '=', $user->roles->first()?->name)->get(['id', 'name']),
|
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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->store($request->validated()),
|
||||||
'Pegawai berhasil ditambahkan.',
|
'Pegawai berhasil ditambahkan.',
|
||||||
'admin.hr.employees.index'
|
'admin.hr.employees.index'
|
||||||
);
|
);
|
||||||
@ -59,10 +58,8 @@ public function edit(User $user): Response
|
|||||||
|
|
||||||
return Inertia::render('admin/hr/employee/edit', [
|
return Inertia::render('admin/hr/employee/edit', [
|
||||||
'employee' => $user,
|
'employee' => $user,
|
||||||
'roles' => Role::where('name', '!=', 'Developer')
|
'roles' => $this->roleService->getForEmployee(),
|
||||||
->when($this->service->shouldHideAdminBahanBaku(), fn ($q) => $q->where('name', '!=', 'admin-bahan-baku'))
|
'canViewAll' => self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]),
|
||||||
->get(['id', 'name']),
|
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +75,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->destroy($user),
|
||||||
'Pegawai berhasil dihapus.',
|
'Pegawai berhasil dihapus.',
|
||||||
'admin.hr.employees.index'
|
'admin.hr.employees.index'
|
||||||
);
|
);
|
||||||
@ -91,7 +88,7 @@ public function toggleActive(User $user): RedirectResponse
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => "Pegawai berhasil {$status}."]);
|
Inertia::flash('toast', ['type' => 'success', 'message' => "Pegawai berhasil {$status}."]);
|
||||||
|
|
||||||
return to_route('admin.hr.employees.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetPassword(User $user): RedirectResponse
|
public function resetPassword(User $user): RedirectResponse
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function store(LeaveRequestRequest $request): RedirectResponse
|
public function store(LeaveRequestRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Permohonan cuti berhasil ditambahkan.',
|
'Permohonan cuti berhasil ditambahkan.',
|
||||||
'admin.hr.leave-requests.index'
|
'admin.hr.leave-requests.index'
|
||||||
);
|
);
|
||||||
@ -53,7 +53,7 @@ public function update(LeaveRequestRequest $request, LeaveRequest $leaveRequest)
|
|||||||
public function destroy(LeaveRequest $leaveRequest): RedirectResponse
|
public function destroy(LeaveRequest $leaveRequest): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($leaveRequest),
|
fn () => $this->service->destroy($leaveRequest),
|
||||||
'Permohonan cuti berhasil dihapus.',
|
'Permohonan cuti berhasil dihapus.',
|
||||||
'admin.hr.leave-requests.index'
|
'admin.hr.leave-requests.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Cutting;
|
use App\Models\Cutting;
|
||||||
use App\Services\Admin\Manage\CuttingService;
|
use App\Services\Admin\Manage\CuttingService;
|
||||||
|
use App\Services\Admin\Master\RawMaterial\RawMaterialVariantService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -15,6 +16,7 @@ class CuttingController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CuttingService $service,
|
private CuttingService $service,
|
||||||
|
private RawMaterialVariantService $rawMaterialVariantService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -29,14 +31,14 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/cutting/create', [
|
return Inertia::render('admin/manage/cutting/create', [
|
||||||
'data' => $this->service->getForCreate(),
|
'rawMaterials' => $this->rawMaterialVariantService->getForCutting(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(CuttingRequest $request): RedirectResponse
|
public function store(CuttingRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Cutting berhasil ditambahkan.',
|
'Cutting berhasil ditambahkan.',
|
||||||
'admin.manage.cuttings.index',
|
'admin.manage.cuttings.index',
|
||||||
'admin.manage.cuttings.create'
|
'admin.manage.cuttings.create'
|
||||||
@ -47,7 +49,7 @@ public function edit(Cutting $cutting): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/cutting/edit', [
|
return Inertia::render('admin/manage/cutting/edit', [
|
||||||
'cutting' => $this->service->getForEdit($cutting),
|
'cutting' => $this->service->getForEdit($cutting),
|
||||||
'data' => $this->service->getForCreate(),
|
'rawMaterials' => $this->rawMaterialVariantService->getForCutting(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ public function update(CuttingRequest $request, Cutting $cutting): RedirectRespo
|
|||||||
public function destroy(Cutting $cutting): RedirectResponse
|
public function destroy(Cutting $cutting): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($cutting),
|
fn () => $this->service->destroy($cutting),
|
||||||
'Cutting berhasil dihapus.',
|
'Cutting berhasil dihapus.',
|
||||||
'admin.manage.cuttings.index'
|
'admin.manage.cuttings.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Services\Admin\Manage\PurchaseService;
|
use App\Services\Admin\Manage\PurchaseService;
|
||||||
|
use App\Services\Admin\Master\RawMaterial\RawMaterialVariantService;
|
||||||
use App\Services\Admin\Master\SupplierService;
|
use App\Services\Admin\Master\SupplierService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -17,6 +18,7 @@ class PurchaseController extends Controller
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PurchaseService $service,
|
private readonly PurchaseService $service,
|
||||||
private readonly SupplierService $supplierService,
|
private readonly SupplierService $supplierService,
|
||||||
|
private readonly RawMaterialVariantService $rawMaterialVariantService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -34,14 +36,15 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/purchase/create', [
|
return Inertia::render('admin/manage/purchase/create', [
|
||||||
'data' => $this->service->getForCreate(),
|
'suppliers' => $this->supplierService->getAll(),
|
||||||
|
'rawMaterials' => $this->rawMaterialVariantService->getForCutting(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(PurchaseRequest $request): RedirectResponse
|
public function store(PurchaseRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Belanja berhasil ditambahkan.',
|
'Belanja berhasil ditambahkan.',
|
||||||
'admin.manage.purchases.index',
|
'admin.manage.purchases.index',
|
||||||
'admin.manage.purchases.create'
|
'admin.manage.purchases.create'
|
||||||
@ -52,7 +55,8 @@ public function edit(Purchase $purchase): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/purchase/edit', [
|
return Inertia::render('admin/manage/purchase/edit', [
|
||||||
'purchase' => $this->service->getForEdit($purchase),
|
'purchase' => $this->service->getForEdit($purchase),
|
||||||
'data' => $this->service->getForCreate(),
|
'suppliers' => $this->supplierService->getAll(),
|
||||||
|
'rawMaterials' => $this->rawMaterialVariantService->getForCutting(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
|||||||
public function destroy(Purchase $purchase): RedirectResponse
|
public function destroy(Purchase $purchase): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($purchase),
|
fn () => $this->service->destroy($purchase),
|
||||||
'Belanja berhasil dihapus.',
|
'Belanja berhasil dihapus.',
|
||||||
'admin.manage.purchases.index'
|
'admin.manage.purchases.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Restock;
|
use App\Models\Restock;
|
||||||
use App\Services\Admin\Manage\RestockService;
|
use App\Services\Admin\Manage\RestockService;
|
||||||
|
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -15,6 +16,7 @@ class RestockController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RestockService $service,
|
private RestockService $service,
|
||||||
|
private ProductVariantService $productVariantService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -29,14 +31,14 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/restock/create', [
|
return Inertia::render('admin/manage/restock/create', [
|
||||||
'data' => $this->service->getForCreate(),
|
'products' => $this->productVariantService->getForRestock(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(RestockRequest $request): RedirectResponse
|
public function store(RestockRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Restock berhasil ditambahkan.',
|
'Restock berhasil ditambahkan.',
|
||||||
'admin.manage.restocks.index',
|
'admin.manage.restocks.index',
|
||||||
'admin.manage.restocks.create'
|
'admin.manage.restocks.create'
|
||||||
@ -47,7 +49,7 @@ public function edit(Restock $restock): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/restock/edit', [
|
return Inertia::render('admin/manage/restock/edit', [
|
||||||
'restock' => $this->service->getForEdit($restock),
|
'restock' => $this->service->getForEdit($restock),
|
||||||
'data' => $this->service->getForCreate(),
|
'products' => $this->productVariantService->getForRestock(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ public function update(RestockRequest $request, Restock $restock): RedirectRespo
|
|||||||
public function destroy(Restock $restock): RedirectResponse
|
public function destroy(Restock $restock): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($restock),
|
fn () => $this->service->destroy($restock),
|
||||||
'Restock berhasil dihapus.',
|
'Restock berhasil dihapus.',
|
||||||
'admin.manage.restocks.index'
|
'admin.manage.restocks.index'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Manage;
|
namespace App\Http\Controllers\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\OrderChannel;
|
||||||
|
use App\Enums\PaymentType;
|
||||||
|
use App\Enums\PriceType;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Manage\TransactionRequest;
|
use App\Http\Requests\Admin\Manage\TransactionRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Admin\Manage\TransactionService;
|
use App\Services\Admin\Manage\TransactionService;
|
||||||
|
use App\Services\Admin\Master\CustomerService;
|
||||||
|
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -15,6 +21,8 @@ class TransactionController extends Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TransactionService $service,
|
private TransactionService $service,
|
||||||
|
private ProductVariantService $productVariantService,
|
||||||
|
private CustomerService $customerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
@ -35,14 +43,19 @@ public function index(PaginatedRequest $request): Response
|
|||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/transaction/create', [
|
return Inertia::render('admin/manage/transaction/create', [
|
||||||
'data' => $this->service->getForCreate(),
|
'products' => $this->productVariantService->getForTransaction(),
|
||||||
|
'customers' => $this->customerService->getAll(),
|
||||||
|
'employees' => $this->getEmployees(),
|
||||||
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
|
'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(TransactionRequest $request): RedirectResponse
|
public function store(TransactionRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Transaksi berhasil ditambahkan.',
|
'Transaksi berhasil ditambahkan.',
|
||||||
'admin.manage.transactions.index',
|
'admin.manage.transactions.index',
|
||||||
'admin.manage.transactions.create'
|
'admin.manage.transactions.create'
|
||||||
@ -53,7 +66,12 @@ public function edit(Order $transaction): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('admin/manage/transaction/edit', [
|
return Inertia::render('admin/manage/transaction/edit', [
|
||||||
'transaction' => $this->service->getForEdit($transaction),
|
'transaction' => $this->service->getForEdit($transaction),
|
||||||
'data' => $this->service->getForCreate(),
|
'products' => $this->productVariantService->getForTransaction(),
|
||||||
|
'customers' => $this->customerService->getAll(),
|
||||||
|
'employees' => $this->getEmployees(),
|
||||||
|
'channelOptions' => OrderChannel::toSelect(),
|
||||||
|
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||||
|
'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +89,7 @@ public function update(TransactionRequest $request, Order $transaction): Redirec
|
|||||||
public function destroy(Order $transaction): RedirectResponse
|
public function destroy(Order $transaction): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($transaction),
|
fn () => $this->service->destroy($transaction),
|
||||||
'Transaksi berhasil dihapus.',
|
'Transaksi berhasil dihapus.',
|
||||||
'admin.manage.transactions.index'
|
'admin.manage.transactions.index'
|
||||||
);
|
);
|
||||||
@ -85,4 +103,16 @@ public function updateStatus(Order $transaction): RedirectResponse
|
|||||||
'admin.manage.transactions.index'
|
'admin.manage.transactions.index'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getEmployees()
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->select('id')
|
||||||
|
->active()
|
||||||
|
->with('userProfile:id,user_id,full_name')
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->filter(fn (User $user) => $user->userProfile?->full_name)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,11 +26,11 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function store(CategoryRequest $request): RedirectResponse
|
public function store(CategoryRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->create($request->validated());
|
$this->service->store($request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil ditambahkan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil ditambahkan.']);
|
||||||
|
|
||||||
return to_route('admin.master.categories.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(CategoryRequest $request, Category $category): RedirectResponse
|
public function update(CategoryRequest $request, Category $category): RedirectResponse
|
||||||
@ -39,15 +39,15 @@ public function update(CategoryRequest $request, Category $category): RedirectRe
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.master.categories.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Category $category): RedirectResponse
|
public function destroy(Category $category): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->delete($category);
|
$this->service->destroy($category);
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil dihapus.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kategori berhasil dihapus.']);
|
||||||
|
|
||||||
return to_route('admin.master.categories.index');
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,11 +26,11 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function store(CustomerRequest $request): RedirectResponse
|
public function store(CustomerRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->create($request->validated());
|
$this->service->store($request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil ditambahkan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil ditambahkan.']);
|
||||||
|
|
||||||
return to_route('admin.master.customers.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
|
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
|
||||||
@ -39,15 +39,15 @@ public function update(CustomerRequest $request, Customer $customer): RedirectRe
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.master.customers.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Customer $customer): RedirectResponse
|
public function destroy(Customer $customer): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->delete($customer);
|
$this->service->destroy($customer);
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil dihapus.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil dihapus.']);
|
||||||
|
|
||||||
return to_route('admin.master.customers.index');
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,6 +84,34 @@ public function toggleStatus(Product $product): RedirectResponse
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => "Status produk berhasil diubah menjadi {$status->label()}."]);
|
Inertia::flash('toast', ['type' => 'success', 'message' => "Status produk berhasil diubah menjadi {$status->label()}."]);
|
||||||
|
|
||||||
return to_route('admin.master.products.index');
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(Product $product): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->approve($product);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil disetujui.']);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Product $product): RedirectResponse
|
||||||
|
{
|
||||||
|
$reason = request()->input('rejection_reason', '');
|
||||||
|
$this->service->reject($product, $reason);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil ditolak.']);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resubmit(Product $product): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->resubmit($product);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Produk berhasil diajukan ulang.']);
|
||||||
|
|
||||||
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,6 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response
|
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response
|
||||||
{
|
{
|
||||||
$perPage = $request->validatedWithDefaults()['perPage'];
|
|
||||||
|
|
||||||
return Inertia::render('admin/master/product/variant/stock-mutations', [
|
return Inertia::render('admin/master/product/variant/stock-mutations', [
|
||||||
'product' => [
|
'product' => [
|
||||||
'id' => $product->id,
|
'id' => $product->id,
|
||||||
@ -29,7 +27,7 @@ public function index(StockMutationRequest $request, Product $product, ProductVa
|
|||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
'name' => $variant->name,
|
'name' => $variant->name,
|
||||||
],
|
],
|
||||||
'mutations' => $this->service->paginated($variant, $perPage),
|
'mutations' => $this->service->paginated($variant, ...$request->validatedWithDefaults()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ public function create(): Response
|
|||||||
public function store(RawMaterialRequest $request): RedirectResponse
|
public function store(RawMaterialRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Bahan baku berhasil ditambahkan.',
|
'Bahan baku berhasil ditambahkan.',
|
||||||
'admin.master.raw-materials.index',
|
'admin.master.raw-materials.index',
|
||||||
'admin.master.raw-materials.create'
|
'admin.master.raw-materials.create'
|
||||||
@ -64,7 +64,7 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
|
|||||||
public function destroy(RawMaterial $rawMaterial): RedirectResponse
|
public function destroy(RawMaterial $rawMaterial): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->delete($rawMaterial),
|
fn () => $this->service->destroy($rawMaterial),
|
||||||
'Bahan baku berhasil dihapus.',
|
'Bahan baku berhasil dihapus.',
|
||||||
'admin.master.raw-materials.index'
|
'admin.master.raw-materials.index'
|
||||||
);
|
);
|
||||||
@ -77,6 +77,6 @@ public function toggleStatus(RawMaterial $rawMaterial): RedirectResponse
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => "Status bahan baku berhasil diubah menjadi {$status}."]);
|
Inertia::flash('toast', ['type' => 'success', 'message' => "Status bahan baku berhasil diubah menjadi {$status}."]);
|
||||||
|
|
||||||
return to_route('admin.master.raw-materials.index');
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,11 +26,11 @@ public function index(PaginatedRequest $request): Response
|
|||||||
|
|
||||||
public function store(SupplierRequest $request): RedirectResponse
|
public function store(SupplierRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->create($request->validated());
|
$this->service->store($request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil ditambahkan.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil ditambahkan.']);
|
||||||
|
|
||||||
return to_route('admin.master.suppliers.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(SupplierRequest $request, Supplier $supplier): RedirectResponse
|
public function update(SupplierRequest $request, Supplier $supplier): RedirectResponse
|
||||||
@ -39,15 +39,15 @@ public function update(SupplierRequest $request, Supplier $supplier): RedirectRe
|
|||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil diperbarui.']);
|
||||||
|
|
||||||
return to_route('admin.master.suppliers.index');
|
return back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Supplier $supplier): RedirectResponse
|
public function destroy(Supplier $supplier): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->delete($supplier);
|
$this->service->destroy($supplier);
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil dihapus.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Supplier berhasil dihapus.']);
|
||||||
|
|
||||||
return to_route('admin.master.suppliers.index');
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public function create(): Response
|
|||||||
public function store(RoleRequest $request): RedirectResponse
|
public function store(RoleRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->create($request->validated()),
|
fn () => $this->service->store($request->validated()),
|
||||||
'Role berhasil ditambahkan.',
|
'Role berhasil ditambahkan.',
|
||||||
'admin.settings.roles.index'
|
'admin.settings.roles.index'
|
||||||
);
|
);
|
||||||
@ -43,7 +43,7 @@ public function store(RoleRequest $request): RedirectResponse
|
|||||||
public function edit(Role $role): Response
|
public function edit(Role $role): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/roles/edit', [
|
return Inertia::render('admin/roles/edit', [
|
||||||
'role' => $this->service->getById($role->id),
|
'role' => $role->load('permissions'),
|
||||||
'permissions' => $this->service->getPermissionsByModule(),
|
'permissions' => $this->service->getPermissionsByModule(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -59,10 +59,10 @@ public function update(RoleRequest $request, Role $role): RedirectResponse
|
|||||||
|
|
||||||
public function destroy(Role $role): RedirectResponse
|
public function destroy(Role $role): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->service->delete($role);
|
$this->service->destroy($role);
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Role berhasil dihapus.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Role berhasil dihapus.']);
|
||||||
|
|
||||||
return to_route('admin.settings.roles.index');
|
return back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
app/Http/Controllers/AnalysisController.php
Normal file
73
app/Http/Controllers/AnalysisController.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Services\AnalysisService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class AnalysisController extends Controller
|
||||||
|
{
|
||||||
|
use HasRoleChecks;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private AnalysisService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$isManager = self::hasAnyRole([Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO, Role::DIREKTUR]);
|
||||||
|
|
||||||
|
$startDate = $request->input('start_date');
|
||||||
|
$endDate = $request->input('end_date');
|
||||||
|
|
||||||
|
$attendance = $this->service->getAttendanceStats($startDate, $endDate);
|
||||||
|
$myAttendance = $this->service->getMyAttendance($user, $startDate, $endDate);
|
||||||
|
$cashOverview = $this->service->getCashOverview($startDate, $endDate);
|
||||||
|
$rawMaterialStock = $this->service->getRawMaterialStock();
|
||||||
|
$productStock = $this->service->getProductStock();
|
||||||
|
$revenueSummary = $this->service->getRevenueSummary($startDate, $endDate);
|
||||||
|
$monthlyRevenue = $this->service->getMonthlyRevenue($startDate, $endDate);
|
||||||
|
$monthlyRevenueByChannel = $this->service->getMonthlyRevenueByChannel($startDate, $endDate);
|
||||||
|
$revenueByPaymentType = $this->service->getRevenueByPaymentType($startDate, $endDate);
|
||||||
|
$expenseSummary = $this->service->getExpenseSummary($startDate, $endDate);
|
||||||
|
$monthlyExpense = $this->service->getMonthlyExpense($startDate, $endDate);
|
||||||
|
$busyHours = $this->service->getBusyHours($startDate, $endDate);
|
||||||
|
$profitMetrics = $this->service->getProfitMetrics($startDate, $endDate);
|
||||||
|
$topSuppliers = $this->service->getTopSuppliers($startDate, $endDate);
|
||||||
|
$topCustomers = $this->service->getTopCustomers($startDate, $endDate);
|
||||||
|
$topProducts = $this->service->getTopProducts($startDate, $endDate);
|
||||||
|
$marketingSales = $this->service->getMarketingSales($startDate, $endDate);
|
||||||
|
$orderStats = $this->service->getOrderStats($startDate, $endDate);
|
||||||
|
|
||||||
|
return Inertia::render('admin/analysis/index', [
|
||||||
|
'filters' => [
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
],
|
||||||
|
'attendance' => $attendance,
|
||||||
|
'myAttendance' => $myAttendance,
|
||||||
|
'isManager' => $isManager,
|
||||||
|
'cashOverview' => $cashOverview,
|
||||||
|
'rawMaterialStock' => $rawMaterialStock,
|
||||||
|
'productStock' => $productStock,
|
||||||
|
'revenueSummary' => $revenueSummary,
|
||||||
|
'monthlyRevenue' => $monthlyRevenue,
|
||||||
|
'monthlyRevenueByChannel' => $monthlyRevenueByChannel,
|
||||||
|
'revenueByPaymentType' => $revenueByPaymentType,
|
||||||
|
'expenseSummary' => $expenseSummary,
|
||||||
|
'monthlyExpense' => $monthlyExpense,
|
||||||
|
'busyHours' => $busyHours,
|
||||||
|
'profitMetrics' => $profitMetrics,
|
||||||
|
'topSuppliers' => $topSuppliers,
|
||||||
|
'topCustomers' => $topCustomers,
|
||||||
|
'topProducts' => $topProducts,
|
||||||
|
'marketingSales' => $marketingSales,
|
||||||
|
'orderStats' => $orderStats,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Http/Controllers/DashboardController.php
Normal file
30
app/Http/Controllers/DashboardController.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\DashboardService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DashboardService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return Inertia::render('dashboard', [
|
||||||
|
'attendance' => $this->service->getAttendanceStats(),
|
||||||
|
'revenueSummary' => $this->service->getRevenueSummary(),
|
||||||
|
'expenseSummary' => $this->service->getExpenseSummary(),
|
||||||
|
'orderStats' => $this->service->getOrderStats(),
|
||||||
|
'todayAttendance' => $this->service->getTodayAttendance($user),
|
||||||
|
'isOnLeave' => $this->service->isOnLeave($user),
|
||||||
|
'canCheckIn' => $user->employee !== null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
app/Http/Controllers/HomepageController.php
Normal file
127
app/Http/Controllers/HomepageController.php
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Services\S3PresignedService;
|
||||||
|
use App\Settings\HomepageSettings;
|
||||||
|
use App\Settings\SocialMediaSettings;
|
||||||
|
use App\Settings\SystemSettings;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class HomepageController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private S3PresignedService $s3Service,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
$system = app(SystemSettings::class);
|
||||||
|
$homepage = app(HomepageSettings::class);
|
||||||
|
$socialMedia = app(SocialMediaSettings::class);
|
||||||
|
|
||||||
|
$categories = Category::select(['id', 'name', 'slug'])
|
||||||
|
->orderBy('name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$search = $request->input('search', '');
|
||||||
|
$category = $request->input('category', '');
|
||||||
|
|
||||||
|
$productsQuery = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
||||||
|
->active()
|
||||||
|
->with([
|
||||||
|
'categories:id,name,slug',
|
||||||
|
'productVariants:id,product_id,name,stock',
|
||||||
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($search !== '') {
|
||||||
|
$productsQuery->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('description', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category !== '') {
|
||||||
|
$productsQuery->whereHas('categories', function ($q) use ($category) {
|
||||||
|
$q->where('slug', $category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = Inertia::scroll(
|
||||||
|
fn () => $productsQuery->orderBy('created_at', 'desc')->paginate(12)
|
||||||
|
);
|
||||||
|
|
||||||
|
$galleryImages = array_map(
|
||||||
|
fn ($key) => str_starts_with($key, 'http') ? $key : $this->s3Service->getTemporaryUrl($key, 60),
|
||||||
|
$homepage->gallery_images ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Inertia::render('welcome', [
|
||||||
|
'appName' => $system->app_name,
|
||||||
|
'aboutApp' => $system->about_app,
|
||||||
|
'contactEmail' => $system->email,
|
||||||
|
'contactPhone' => $system->phone,
|
||||||
|
'contactAddress' => $system->address,
|
||||||
|
'instagramUrl' => $socialMedia->instagram_url,
|
||||||
|
'facebookUrl' => $socialMedia->facebook_url,
|
||||||
|
'tiktokUrl' => $socialMedia->tiktok_url,
|
||||||
|
'homepage' => [
|
||||||
|
'hero_badge' => $homepage->hero_badge,
|
||||||
|
'hero_title_line1' => $homepage->hero_title_line1,
|
||||||
|
'hero_title_line2' => $homepage->hero_title_line2,
|
||||||
|
'hero_title_highlight' => $homepage->hero_title_highlight,
|
||||||
|
'hero_description' => $homepage->hero_description,
|
||||||
|
'hero_cta_primary_text' => $homepage->hero_cta_primary_text,
|
||||||
|
'hero_cta_secondary_text' => $homepage->hero_cta_secondary_text,
|
||||||
|
'hero_image_url' => $homepage->hero_image_url
|
||||||
|
? (str_starts_with($homepage->hero_image_url, 'http') ? $homepage->hero_image_url : $this->s3Service->getTemporaryUrl($homepage->hero_image_url, 60))
|
||||||
|
: null,
|
||||||
|
'hero_bg_text_left' => $homepage->hero_bg_text_left,
|
||||||
|
'hero_bg_text_right' => $homepage->hero_bg_text_right,
|
||||||
|
'scroll_hashtag' => $homepage->scroll_hashtag,
|
||||||
|
'scroll_tagline' => $homepage->scroll_tagline,
|
||||||
|
'catalog_badge' => $homepage->catalog_badge,
|
||||||
|
'catalog_title' => $homepage->catalog_title,
|
||||||
|
'catalog_description' => $homepage->catalog_description,
|
||||||
|
'catalog_search_placeholder' => $homepage->catalog_search_placeholder,
|
||||||
|
'gallery_badge' => $homepage->gallery_badge,
|
||||||
|
'gallery_title' => $homepage->gallery_title,
|
||||||
|
'gallery_description' => $homepage->gallery_description,
|
||||||
|
'gallery_images' => $galleryImages,
|
||||||
|
'order_guide_badge' => $homepage->order_guide_badge,
|
||||||
|
'order_guide_title' => $homepage->order_guide_title,
|
||||||
|
'order_guide_description' => $homepage->order_guide_description,
|
||||||
|
'order_steps' => $homepage->order_steps,
|
||||||
|
'about_badge' => $homepage->about_badge,
|
||||||
|
'about_title' => $homepage->about_title,
|
||||||
|
'about_image_url' => $homepage->about_image_url
|
||||||
|
? (str_starts_with($homepage->about_image_url, 'http') ? $homepage->about_image_url : $this->s3Service->getTemporaryUrl($homepage->about_image_url, 60))
|
||||||
|
: null,
|
||||||
|
'about_features' => $homepage->about_features,
|
||||||
|
'contact_badge' => $homepage->contact_badge,
|
||||||
|
'contact_title' => $homepage->contact_title,
|
||||||
|
'contact_description' => $homepage->contact_description,
|
||||||
|
'contact_form_title' => $homepage->contact_form_title,
|
||||||
|
'footer_description' => $homepage->footer_description,
|
||||||
|
'footer_copyright' => $homepage->footer_copyright,
|
||||||
|
],
|
||||||
|
'categories' => $categories,
|
||||||
|
'products' => $products,
|
||||||
|
'filters' => [
|
||||||
|
'search' => $search,
|
||||||
|
'category' => $category,
|
||||||
|
],
|
||||||
|
'seo' => [
|
||||||
|
'title' => $system->app_name.' - '.$homepage->hero_badge,
|
||||||
|
'description' => $homepage->hero_description,
|
||||||
|
'image' => url('/assets/logo.png'),
|
||||||
|
'url' => url('/'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Settings\SystemSettings;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Middleware;
|
use Inertia\Middleware;
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ public function share(Request $request): array
|
|||||||
return [
|
return [
|
||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
'name' => config('app.name'),
|
'name' => config('app.name'),
|
||||||
|
'address' => app(SystemSettings::class)->address ?? '',
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user()
|
'user' => $request->user()
|
||||||
? tap($request->user()->load('userProfile', 'roles'), function ($user) {
|
? tap($request->user()->load('userProfile', 'roles'), function ($user) {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class CashAccountRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('cash.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -12,7 +12,8 @@ class CashTransactionRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('cash.deposit')
|
||||||
|
|| $this->user()->can('cash.withdraw');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class EmployeeAdvancePaymentRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('employee_advances.pay');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@ -12,7 +12,8 @@ class EmployeeAdvanceRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('employee_advances.create')
|
||||||
|
|| $this->user()->can('employee_advances.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@ -12,7 +12,8 @@ class ExpenseRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('expenses.create')
|
||||||
|
|| $this->user()->can('expenses.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class PayrollAdjustmentRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('payroll.adjust');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
@ -29,7 +29,6 @@ public function rules(): array
|
|||||||
'type' => ['required', Rule::in(PayrollAdjustmentType::values())],
|
'type' => ['required', Rule::in(PayrollAdjustmentType::values())],
|
||||||
'amount' => ['required', 'integer', 'min:1'],
|
'amount' => ['required', 'integer', 'min:1'],
|
||||||
'description' => ['required', 'string', 'max:100'],
|
'description' => ['required', 'string', 'max:100'],
|
||||||
'attendance_id' => ['nullable', 'integer', 'exists:attendances,id'],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +38,6 @@ public function attributes(): array
|
|||||||
'type' => 'jenis',
|
'type' => 'jenis',
|
||||||
'amount' => 'jumlah',
|
'amount' => 'jumlah',
|
||||||
'description' => 'keterangan',
|
'description' => 'keterangan',
|
||||||
'attendance_id' => 'presensi',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,15 +8,15 @@ class AttendanceRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('attendances.create');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'photo' => ['required'],
|
'photo' => ['required'],
|
||||||
'latitude' => ['required', 'numeric'],
|
'latitude' => ['required', 'numeric', 'min:-90', 'max:90'],
|
||||||
'longitude' => ['required', 'numeric'],
|
'longitude' => ['required', 'numeric', 'min:-180', 'max:180'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin\HR;
|
namespace App\Http\Requests\Admin\HR;
|
||||||
|
|
||||||
|
use App\Enums\EmploymentStatus;
|
||||||
|
use App\Enums\Gender;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@ -9,7 +11,8 @@ class EmployeeRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('employees.create')
|
||||||
|
|| $this->user()->can('employees.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -34,7 +37,7 @@ public function rules(): array
|
|||||||
'role' => ['required', 'string', Rule::exists('roles', 'name')],
|
'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', Rule::in(Gender::values())],
|
||||||
'birth_date' => ['nullable', 'date'],
|
'birth_date' => ['nullable', 'date'],
|
||||||
'address' => ['nullable', 'string'],
|
'address' => ['nullable', 'string'],
|
||||||
'join_date' => [
|
'join_date' => [
|
||||||
@ -48,7 +51,7 @@ public function rules(): array
|
|||||||
],
|
],
|
||||||
'employment_status' => [
|
'employment_status' => [
|
||||||
Rule::requiredIf(! $isOwner),
|
Rule::requiredIf(! $isOwner),
|
||||||
Rule::in(['full_time', 'part_time', 'contract', 'internship', 'resigned']),
|
Rule::in(EmploymentStatus::values()),
|
||||||
],
|
],
|
||||||
'base_salary' => [
|
'base_salary' => [
|
||||||
Rule::requiredIf(! $isOwner),
|
Rule::requiredIf(! $isOwner),
|
||||||
|
|||||||
@ -2,19 +2,46 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin\HR;
|
namespace App\Http\Requests\Admin\HR;
|
||||||
|
|
||||||
|
use App\Models\LeaveRequest;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class LeaveRequestRequest extends FormRequest
|
class LeaveRequestRequest extends FormRequest
|
||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('leave_requests.create')
|
||||||
|
|| $this->user()->can('leave_requests.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$leaveRequest = $this->route('leaveRequest');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'start_date' => ['required', 'date', 'after_or_equal:today'],
|
'start_date' => [
|
||||||
|
'required',
|
||||||
|
'date',
|
||||||
|
'after:today',
|
||||||
|
function ($attribute, $value, $fail) use ($leaveRequest) {
|
||||||
|
$employee = $this->user()->employee;
|
||||||
|
|
||||||
|
if (! $employee) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = LeaveRequest::where('employee_id', $employee->id)
|
||||||
|
->where('start_date', $value)
|
||||||
|
->where('status', '!=', LeaveRequest::cancelled());
|
||||||
|
|
||||||
|
if ($leaveRequest) {
|
||||||
|
$query->where('id', '!=', $leaveRequest->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->exists()) {
|
||||||
|
$fail('Anda sudah mengajukan cuti pada tanggal ini.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
'end_date' => ['required', 'date', 'after_or_equal:start_date'],
|
'end_date' => ['required', 'date', 'after_or_equal:start_date'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,14 @@
|
|||||||
namespace App\Http\Requests\Admin\Manage;
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class CuttingRequest extends FormRequest
|
class CuttingRequest extends FormRequest
|
||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('cuttings.create')
|
||||||
|
|| $this->user()->can('cuttings.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -20,7 +22,7 @@ public function rules(): array
|
|||||||
'original_outside_sample' => ['required', 'integer'],
|
'original_outside_sample' => ['required', 'integer'],
|
||||||
'cutting_result' => ['required', 'integer', 'min:1'],
|
'cutting_result' => ['required', 'integer', 'min:1'],
|
||||||
'materials' => ['required', 'array', 'min:1'],
|
'materials' => ['required', 'array', 'min:1'],
|
||||||
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
'materials.*.raw_material_price_id' => ['required', 'integer', Rule::exists('raw_material_prices', 'id')],
|
||||||
'materials.*.material_usage' => ['required', 'integer', 'min:1'],
|
'materials.*.material_usage' => ['required', 'integer', 'min:1'],
|
||||||
'materials.*.material_result' => ['required', 'integer'],
|
'materials.*.material_result' => ['required', 'integer'],
|
||||||
'materials.*.combination_index' => ['nullable', 'integer'],
|
'materials.*.combination_index' => ['nullable', 'integer'],
|
||||||
|
|||||||
@ -13,7 +13,8 @@ class PurchaseRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('purchases.create')
|
||||||
|
|| $this->user()->can('purchases.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareForValidation(): void
|
public function prepareForValidation(): void
|
||||||
@ -25,18 +26,18 @@ public function rules(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'mode' => ['sometimes', 'required', 'in:new,existing'],
|
'mode' => ['sometimes', 'required', 'in:new,existing'],
|
||||||
'name' => ['required_unless:mode,existing', 'string', 'max:200'],
|
'name' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:200'],
|
||||||
'unit' => [$this->isMethod('post') ? 'required_unless:mode,existing' : 'nullable', Rule::in(RawMaterialUnit::values())],
|
'unit' => [$this->isMethod('post') ? Rule::requiredUnless(fn () => $this->input('mode') === 'existing') : 'nullable', Rule::in(RawMaterialUnit::values())],
|
||||||
'variants' => ['required_unless:mode,existing', 'array', 'min:1'],
|
'variants' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'],
|
||||||
'variants.*.variant' => ['required_unless:mode,existing', 'string', 'max:200'],
|
'variants.*.variant' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:200'],
|
||||||
'variants.*.price' => ['required_unless:mode,existing', 'integer', 'min:0'],
|
'variants.*.price' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'integer', 'min:0'],
|
||||||
'variants.*.stock' => ['required_unless:mode,existing', 'integer', 'min:0'],
|
'variants.*.stock' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'integer', 'min:0'],
|
||||||
'variants.*.photo_key' => ['required_unless:mode,existing', 'string', 'max:500'],
|
'variants.*.photo_key' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:500'],
|
||||||
'existing_items' => ['required_if:mode,existing', 'array', 'min:1'],
|
'existing_items' => [Rule::requiredIf(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'],
|
||||||
'existing_items.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
'existing_items.*.raw_material_price_id' => ['required', 'integer', Rule::exists('raw_material_prices', 'id')],
|
||||||
'existing_items.*.quantity' => ['required', 'integer', 'min:1'],
|
'existing_items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
'existing_items.*.unit_price' => ['required', 'integer', 'min:0'],
|
'existing_items.*.unit_price' => ['required', 'integer', 'min:0'],
|
||||||
'supplier_id' => ['required', 'integer', 'exists:suppliers,id'],
|
'supplier_id' => ['required', 'integer', Rule::exists('suppliers', 'id')],
|
||||||
'discount' => ['nullable', 'integer', 'min:0'],
|
'discount' => ['nullable', 'integer', 'min:0'],
|
||||||
'shipping_cost' => ['nullable', 'integer', 'min:0'],
|
'shipping_cost' => ['nullable', 'integer', 'min:0'],
|
||||||
'notes' => ['nullable', 'string', 'max:100'],
|
'notes' => ['nullable', 'string', 'max:100'],
|
||||||
|
|||||||
@ -10,7 +10,8 @@ class RestockRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('restocks.create')
|
||||||
|
|| $this->user()->can('restocks.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -18,7 +19,7 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
||||||
'items' => ['required', 'array', 'min:1'],
|
'items' => ['required', 'array', 'min:1'],
|
||||||
'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
|
'items.*.product_variant_id' => ['required', 'integer', Rule::exists('product_variants', 'id')],
|
||||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
'notes' => ['nullable', 'string', 'max:100'],
|
'notes' => ['nullable', 'string', 'max:100'],
|
||||||
'photo_key' => ['nullable', 'string', 'max:500'],
|
'photo_key' => ['nullable', 'string', 'max:500'],
|
||||||
|
|||||||
@ -16,7 +16,8 @@ class TransactionRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('orders.create')
|
||||||
|
|| $this->user()->can('orders.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareForValidation(): void
|
public function prepareForValidation(): void
|
||||||
@ -26,15 +27,13 @@ public function prepareForValidation(): void
|
|||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$sellingPriceTypes = array_diff(PriceType::values(), [PriceType::CAPITAL->value]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
'stock_type' => ['sometimes', 'required', Rule::in(ProductStockQuality::values())],
|
||||||
'channel' => ['sometimes', 'required', Rule::in(OrderChannel::values())],
|
'channel' => ['sometimes', 'required', Rule::in(OrderChannel::values())],
|
||||||
'price_type' => ['sometimes', 'required', Rule::in($sellingPriceTypes)],
|
'price_type' => ['sometimes', 'required', Rule::in(array_diff(PriceType::values(), [PriceType::CAPITAL->value]))],
|
||||||
'payment_type' => ['sometimes', 'required', Rule::in(PaymentType::values())],
|
'payment_type' => ['sometimes', 'required', Rule::in(PaymentType::values())],
|
||||||
'customer_id' => ['nullable', 'integer', 'exists:customers,id'],
|
'customer_id' => ['nullable', 'integer', Rule::exists('customers', 'id')],
|
||||||
'marketing_id' => ['nullable', 'integer', 'exists:users,id'],
|
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')],
|
||||||
'discount' => ['nullable', 'integer', 'min:0'],
|
'discount' => ['nullable', 'integer', 'min:0'],
|
||||||
'nego_price' => ['nullable', 'integer'],
|
'nego_price' => ['nullable', 'integer'],
|
||||||
'is_completed' => ['sometimes', 'boolean'],
|
'is_completed' => ['sometimes', 'boolean'],
|
||||||
@ -42,7 +41,7 @@ public function rules(): array
|
|||||||
'tiktok_order_id' => ['nullable', 'string', 'max:100'],
|
'tiktok_order_id' => ['nullable', 'string', 'max:100'],
|
||||||
'shopee_order_id' => ['nullable', 'string', 'max:100'],
|
'shopee_order_id' => ['nullable', 'string', 'max:100'],
|
||||||
'items' => ['required', 'array', 'min:1'],
|
'items' => ['required', 'array', 'min:1'],
|
||||||
'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
|
'items.*.product_variant_id' => ['required', 'integer', Rule::exists('product_variants', 'id')],
|
||||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
'notes' => ['nullable', 'string', 'max:100'],
|
'notes' => ['nullable', 'string', 'max:100'],
|
||||||
'photo_key' => [
|
'photo_key' => [
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class CategoryRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('categories.create')
|
||||||
|
|| $this->user()->can('categories.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -17,7 +18,7 @@ public function rules(): array
|
|||||||
$category = $this->route('category');
|
$category = $this->route('category');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => ['required', 'string', 'max:100', Rule::unique('categories', 'name')->ignore($category)],
|
'name' => ['required', 'string', 'max:50', Rule::unique('categories', 'name')->ignore($category)],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class CustomerRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('customers.create')
|
||||||
|
|| $this->user()->can('customers.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Concerns\CurrencyStripping;
|
use App\Concerns\CurrencyStripping;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Override;
|
use Override;
|
||||||
@ -14,7 +15,8 @@ class ProductRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('products.create')
|
||||||
|
|| $this->user()->can('products.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
@ -34,13 +36,13 @@ public function rules(): array
|
|||||||
'max:200',
|
'max:200',
|
||||||
],
|
],
|
||||||
'description' => ['nullable', 'string'],
|
'description' => ['nullable', 'string'],
|
||||||
'status' => ['nullable', Rule::in(['active', 'inactive', 'draft'])],
|
'status' => ['nullable', Rule::in(ProductStatus::values())],
|
||||||
'category_ids' => ['required', 'array', 'min:1'],
|
'category_ids' => ['required', 'array', 'min:1'],
|
||||||
'category_ids.*' => ['exists:categories,id'],
|
'category_ids.*' => [Rule::exists('categories', 'id')],
|
||||||
'use_same_price' => ['nullable', 'boolean'],
|
'use_same_price' => ['nullable', 'boolean'],
|
||||||
'shared_prices' => ['required_if:use_same_price,true', 'nullable', 'array', ...($useSamePrice ? ['size:9'] : [])],
|
'shared_prices' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', 'array', ...($useSamePrice ? ['size:9'] : [])],
|
||||||
'shared_prices.*.type' => ['required_if:use_same_price,true', 'nullable', Rule::in(PriceType::values())],
|
'shared_prices.*.type' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||||
'shared_prices.*.price' => ['required_if:use_same_price,true', 'nullable', 'integer', 'min:0'],
|
'shared_prices.*.price' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||||
'variants' => ['required', 'array', 'min:1'],
|
'variants' => ['required', 'array', 'min:1'],
|
||||||
'variants.*.id' => ['nullable', 'integer'],
|
'variants.*.id' => ['nullable', 'integer'],
|
||||||
'variants.*.name' => ['required', 'string', 'max:200'],
|
'variants.*.name' => ['required', 'string', 'max:200'],
|
||||||
@ -49,10 +51,10 @@ public function rules(): array
|
|||||||
'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
|
'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
|
||||||
'variants.*.photo_keys' => ['required', 'array', 'min:1', 'max:5'],
|
'variants.*.photo_keys' => ['required', 'array', 'min:1', 'max:5'],
|
||||||
'variants.*.photo_keys.*' => ['required', 'string', 'max:500'],
|
'variants.*.photo_keys.*' => ['required', 'string', 'max:500'],
|
||||||
'variants.*.prices' => ['required_if:use_same_price,false', 'nullable', 'array', ...(! $useSamePrice ? ['size:9'] : [])],
|
'variants.*.prices' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', 'array', ...(! $useSamePrice ? ['size:9'] : [])],
|
||||||
'variants.*.prices.*.id' => ['nullable', 'integer'],
|
'variants.*.prices.*.id' => ['nullable', 'integer'],
|
||||||
'variants.*.prices.*.type' => ['required_if:use_same_price,false', 'nullable', Rule::in(PriceType::values())],
|
'variants.*.prices.*.type' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||||
'variants.*.prices.*.price' => ['required_if:use_same_price,false', 'nullable', 'integer', 'min:0'],
|
'variants.*.prices.*.price' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,8 @@ class ProductVariantRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('products.create')
|
||||||
|
|| $this->user()->can('products.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareForValidation(): void
|
public function prepareForValidation(): void
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class TransferStockRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('products.transfer_stock');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -13,7 +13,8 @@ class RawMaterialRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('raw_materials.create')
|
||||||
|
|| $this->user()->can('raw_materials.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareForValidation(): void
|
public function prepareForValidation(): void
|
||||||
|
|||||||
@ -11,7 +11,8 @@ class RawMaterialVariantRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('raw_materials.create')
|
||||||
|
|| $this->user()->can('raw_materials.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepareForValidation(): void
|
public function prepareForValidation(): void
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class SupplierRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('suppliers.create')
|
||||||
|
|| $this->user()->can('suppliers.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class RoleRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('roles.create')
|
||||||
|
|| $this->user()->can('roles.update');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -24,7 +25,7 @@ public function rules(): array
|
|||||||
Rule::unique('roles', 'name')->ignore($roleId, 'id'),
|
Rule::unique('roles', 'name')->ignore($roleId, 'id'),
|
||||||
],
|
],
|
||||||
'permissions' => ['present', 'array'],
|
'permissions' => ['present', 'array'],
|
||||||
'permissions.*' => ['string', 'exists:permissions,name'],
|
'permissions.*' => ['string', Rule::exists('permissions', 'name')],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class UpdateHRRequest extends FormRequest
|
|||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('settings.update_hr');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Override]
|
#[Override]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class UpdateHomepageRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('settings.update_homepage');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class UpdateMarketplaceRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('settings.update_marketplace');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function feeRuleRules(): array
|
private function feeRuleRules(): array
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class UpdateSocialMediaRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('settings.update_social_media');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class UpdateSystemRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return $this->user()->can('settings.update_system');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public function rules(): array
|
|||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
'email',
|
'email',
|
||||||
'max:255',
|
'max:100',
|
||||||
Rule::unique('users', 'email')->ignore($userId),
|
Rule::unique('users', 'email')->ignore($userId),
|
||||||
],
|
],
|
||||||
'username' => [
|
'username' => [
|
||||||
|
|||||||
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
#[Appends(['formatted_read_at'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class AppNotification extends Model
|
class AppNotification extends Model
|
||||||
{
|
{
|
||||||
@ -22,6 +25,13 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedReadAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->read_at?->translatedFormat('l, d F Y H:i'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
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\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@ -11,6 +12,7 @@
|
|||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
|
#[Appends(['formatted_attendance_date', 'formatted_check_in_at', 'formatted_check_out_at'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class Attendance extends Model implements HasMedia
|
class Attendance extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
@ -33,7 +35,21 @@ protected function casts(): array
|
|||||||
protected function formattedAttendanceDate(): Attribute
|
protected function formattedAttendanceDate(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn ($value) => $value ? \Carbon\Carbon::parse($value)->translatedFormat('l, d F Y') : null,
|
get: fn () => $this->attendance_date?->translatedFormat('l, d F Y'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedCheckInAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->check_in_at?->translatedFormat('l, d F Y H:i'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedCheckOutAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->check_out_at?->translatedFormat('l, d F Y H:i'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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_material_result', 'formatted_material_usage'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class CuttingMaterial extends Model
|
class CuttingMaterial extends Model
|
||||||
{
|
{
|
||||||
@ -21,6 +24,20 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedMaterialResult(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->material_result, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedMaterialUsage(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->material_usage, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function combination(): BelongsTo
|
public function combination(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(CuttingMaterialCombination::class, 'combination_id');
|
return $this->belongsTo(CuttingMaterialCombination::class, 'combination_id');
|
||||||
|
|||||||
@ -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_material_result'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class CuttingMaterialCombination extends Model
|
class CuttingMaterialCombination extends Model
|
||||||
{
|
{
|
||||||
@ -21,6 +24,13 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedMaterialResult(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->material_result, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function cutting(): BelongsTo
|
public function cutting(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cutting::class);
|
return $this->belongsTo(Cutting::class);
|
||||||
|
|||||||
@ -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_cutting_result', 'formatted_original_outside_sample', 'formatted_sample'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class CuttingResult extends Model
|
class CuttingResult extends Model
|
||||||
{
|
{
|
||||||
@ -22,6 +25,27 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedCuttingResult(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->cutting_result, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedOriginalOutsideSample(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->original_outside_sample, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedSample(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->sample, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function cutting(): BelongsTo
|
public function cutting(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cutting::class);
|
return $this->belongsTo(Cutting::class);
|
||||||
|
|||||||
@ -70,7 +70,7 @@ protected function formattedRemainingAmount(): Attribute
|
|||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->status->label(),
|
get: fn () => $this->status?->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ protected function formattedAmount(): Attribute
|
|||||||
protected function formattedPaidAt(): Attribute
|
protected function formattedPaidAt(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->paid_at?->translatedFormat('l, d F Y'),
|
get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ protected function casts(): array
|
|||||||
protected function channelLabel(): Attribute
|
protected function channelLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->channel->label(),
|
get: fn () => $this->channel?->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,14 +73,14 @@ protected function formattedNegoPrice(): Attribute
|
|||||||
protected function paymentTypeLabel(): Attribute
|
protected function paymentTypeLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->payment_type->label(),
|
get: fn () => $this->payment_type?->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn () => $this->status->label(),
|
get: fn () => $this->status?->label(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
|
||||||
class OwnerVerificationRequest extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'payload' => 'array',
|
|
||||||
'verified_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function subject(): MorphTo
|
|
||||||
{
|
|
||||||
return $this->morphTo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function submittedBy(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'submitted_by_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function verifiedBy(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'verified_by_id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\PayrollPeriodStatus;
|
use App\Enums\PayrollPeriodStatus;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
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;
|
||||||
@ -14,7 +15,7 @@
|
|||||||
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'])]
|
#[Appends(['formatted_closed_at', 'month_name', 'status_label'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class PayrollPeriod extends Model
|
class PayrollPeriod extends Model
|
||||||
{
|
{
|
||||||
@ -30,6 +31,20 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedClosedAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->closed_at?->translatedFormat('l, d F Y H:i'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function monthName(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => $this->month ? Carbon::create()->month($this->month)->translatedFormat('F') : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
@ -61,6 +61,18 @@ protected function inactive(Builder $query): void
|
|||||||
$query->where('status', ProductStatus::INACTIVE);
|
$query->where('status', ProductStatus::INACTIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Scope]
|
||||||
|
protected function pending(Builder $query): void
|
||||||
|
{
|
||||||
|
$query->where('status', ProductStatus::PENDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope]
|
||||||
|
protected function rejected(Builder $query): void
|
||||||
|
{
|
||||||
|
$query->where('status', ProductStatus::REJECTED);
|
||||||
|
}
|
||||||
|
|
||||||
public function categories(): BelongsToMany
|
public function categories(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Category::class, 'product_categories')
|
return $this->belongsToMany(Category::class, 'product_categories')
|
||||||
|
|||||||
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
#[Appends(['formatted_quantity', 'formatted_retail_stock_before', 'formatted_retail_stock_after', 'formatted_stock_before', 'formatted_stock_after'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class RetailStockHistory extends Model
|
class RetailStockHistory extends Model
|
||||||
{
|
{
|
||||||
@ -25,6 +28,41 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedQuantity(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->quantity, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedRetailStockBefore(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->retail_stock_before, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedRetailStockAfter(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->retail_stock_after, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedStockBefore(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->stock_before, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedStockAfter(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->stock_after, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function productVariant(): BelongsTo
|
public function productVariant(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(ProductVariant::class);
|
return $this->belongsTo(ProductVariant::class);
|
||||||
|
|||||||
@ -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\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
#[Appends(['formatted_quantity', 'formatted_stock_after', 'formatted_stock_before'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class StockMutation extends Model
|
class StockMutation extends Model
|
||||||
{
|
{
|
||||||
@ -22,6 +25,27 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedQuantity(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->quantity, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedStockAfter(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->stock_after, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedStockBefore(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => number_format($this->stock_before, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function source(): MorphTo
|
public function source(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
|
|||||||
@ -156,16 +156,6 @@ public function orderItems(): HasMany
|
|||||||
return $this->hasMany(OrderItem::class);
|
return $this->hasMany(OrderItem::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ownerVerificationRequestsSubmitted(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(OwnerVerificationRequest::class, 'submitted_by_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function ownerVerificationRequestsVerified(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(OwnerVerificationRequest::class, 'verified_by_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paidPayrolls(): HasMany
|
public function paidPayrolls(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Payroll::class, 'paid_by_id');
|
return $this->hasMany(Payroll::class, 'paid_by_id');
|
||||||
|
|||||||
@ -24,7 +24,6 @@ public function migrate(): array
|
|||||||
return $row;
|
return $row;
|
||||||
});
|
});
|
||||||
$results['homepage_configurations'] = $this->migrateTable('homepage_configurations');
|
$results['homepage_configurations'] = $this->migrateTable('homepage_configurations');
|
||||||
$results['owner_verification_requests'] = $this->migrateTable('owner_verification_requests');
|
|
||||||
$results['notifications'] = $this->migrateTable('notifications');
|
$results['notifications'] = $this->migrateTable('notifications');
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
|
|||||||
@ -46,15 +46,6 @@ public function getHomepageData(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTemporaryUrl(string $key): string
|
|
||||||
{
|
|
||||||
if (str_starts_with($key, 'http')) {
|
|
||||||
return $key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->s3Service->getTemporaryUrl($key, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSocialMediaData(): array
|
public function getSocialMediaData(): array
|
||||||
{
|
{
|
||||||
$settings = app(SocialMediaSettings::class);
|
$settings = app(SocialMediaSettings::class);
|
||||||
@ -167,4 +158,13 @@ public function updateHR(array $data): void
|
|||||||
$settings->fill($data);
|
$settings->fill($data);
|
||||||
$settings->save();
|
$settings->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getTemporaryUrl(string $key): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($key, 'http')) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->s3Service->getTemporaryUrl($key, 60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
app/Services/Admin/Finance/Cash/CashAccountService.php
Normal file
13
app/Services/Admin/Finance/Cash/CashAccountService.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Admin\Finance\Cash;
|
||||||
|
|
||||||
|
use App\Models\CashAccount;
|
||||||
|
|
||||||
|
class CashAccountService
|
||||||
|
{
|
||||||
|
public function get(): ?CashAccount
|
||||||
|
{
|
||||||
|
return CashAccount::select(['id', 'name', 'balance'])->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance\Cash;
|
||||||
|
|
||||||
use App\Enums\CashTransactionType;
|
use App\Enums\CashTransactionType;
|
||||||
|
use App\Enums\Role;
|
||||||
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\HandlesCashTransactions;
|
||||||
@ -11,45 +12,22 @@
|
|||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator as PaginationLengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator as PaginationLengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class CashAccountService
|
class CashTransactionService
|
||||||
{
|
{
|
||||||
use HandlesCashTransactions, RegistersMedia;
|
use HandlesCashTransactions, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
|
private CashAccountService $cashAccountService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function get(): ?CashAccount
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return CashAccount::select(['id', 'name', 'balance'])->first();
|
$cashAccount = $this->cashAccountService->get();
|
||||||
}
|
|
||||||
|
|
||||||
public function getAllTransactions(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
$cashAccount = $this->get();
|
|
||||||
|
|
||||||
if (! $cashAccount) {
|
|
||||||
return collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cashAccount->cashTransactions()
|
|
||||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
|
||||||
->with('createdBy.userProfile', 'media')
|
|
||||||
->when($filters['type'] ?? null, function ($query, $type) {
|
|
||||||
$query->where('type', $type);
|
|
||||||
})
|
|
||||||
->latest()
|
|
||||||
->get()
|
|
||||||
->map(fn (CashTransaction $transaction) => $this->formatTransaction($transaction));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginatedTransactions(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
$cashAccount = $this->get();
|
|
||||||
|
|
||||||
if (! $cashAccount) {
|
if (! $cashAccount) {
|
||||||
return new PaginationLengthAwarePaginator(collect(), 0, $perPage);
|
return new PaginationLengthAwarePaginator(collect(), 0, $perPage);
|
||||||
@ -70,29 +48,6 @@ public function paginatedTransactions(int $perPage = 25, string $search = '', st
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatTransaction(CashTransaction $transaction): array
|
|
||||||
{
|
|
||||||
$media = $transaction->getFirstMedia('receipts');
|
|
||||||
|
|
||||||
if (! $media) {
|
|
||||||
return $transaction->toArray() + [
|
|
||||||
'receipt_key' => null,
|
|
||||||
'receipt_url' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$s3Key = $media->file_name;
|
|
||||||
|
|
||||||
if (str_starts_with($s3Key, '/') || ! str_contains($s3Key, '/')) {
|
|
||||||
$s3Key = $media->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $transaction->toArray() + [
|
|
||||||
'receipt_key' => $s3Key,
|
|
||||||
'receipt_url' => $this->s3Service->getTemporaryUrl($s3Key),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deposit(array $data): CashTransaction
|
public function deposit(array $data): CashTransaction
|
||||||
{
|
{
|
||||||
$transaction = DB::transaction(fn () => $this->creditCash(
|
$transaction = DB::transaction(fn () => $this->creditCash(
|
||||||
@ -100,12 +55,12 @@ public function deposit(array $data): CashTransaction
|
|||||||
description: $data['description'],
|
description: $data['description'],
|
||||||
));
|
));
|
||||||
|
|
||||||
if (! empty($data['receipt_key'])) {
|
if (array_key_exists('receipt_key', $data)) {
|
||||||
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Setoran Kas Toko',
|
title: 'Setoran Kas Toko',
|
||||||
body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.cash-accounts.index'),
|
url: route('admin.finance.cash-accounts.index'),
|
||||||
@ -122,12 +77,12 @@ public function withdrawal(array $data): CashTransaction
|
|||||||
type: CashTransactionType::WITHDRAWAL,
|
type: CashTransactionType::WITHDRAWAL,
|
||||||
));
|
));
|
||||||
|
|
||||||
if (! empty($data['receipt_key'])) {
|
if (array_key_exists('receipt_key', $data)) {
|
||||||
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Penarikan Kas Toko',
|
title: 'Penarikan Kas Toko',
|
||||||
body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.cash-accounts.index'),
|
url: route('admin.finance.cash-accounts.index'),
|
||||||
@ -136,7 +91,7 @@ public function withdrawal(array $data): CashTransaction
|
|||||||
return $transaction;
|
return $transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateTransaction(CashTransaction $transaction, array $data): CashTransaction
|
public function update(CashTransaction $transaction, array $data): CashTransaction
|
||||||
{
|
{
|
||||||
$transaction = DB::transaction(function () use ($transaction, $data) {
|
$transaction = DB::transaction(function () use ($transaction, $data) {
|
||||||
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
|
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
|
||||||
@ -168,27 +123,14 @@ public function updateTransaction(CashTransaction $transaction, array $data): Ca
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (array_key_exists('receipt_key', $data)) {
|
if (array_key_exists('receipt_key', $data)) {
|
||||||
$currentMedia = $transaction->getFirstMedia('receipts');
|
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||||
$currentKey = $currentMedia?->file_name;
|
|
||||||
|
|
||||||
if ($currentMedia && ! str_contains($currentKey, '/')) {
|
|
||||||
$currentKey = $currentMedia->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['receipt_key'] !== $currentKey) {
|
|
||||||
$transaction->clearMediaCollection('receipts');
|
|
||||||
|
|
||||||
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;
|
return $transaction;
|
||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Transaksi Kas Diperbarui',
|
title: 'Transaksi Kas Diperbarui',
|
||||||
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.cash-accounts.index'),
|
url: route('admin.finance.cash-accounts.index'),
|
||||||
@ -197,7 +139,7 @@ public function updateTransaction(CashTransaction $transaction, array $data): Ca
|
|||||||
return $transaction;
|
return $transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteTransaction(CashTransaction $transaction): bool
|
public function destroy(CashTransaction $transaction): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($transaction) {
|
return DB::transaction(function () use ($transaction) {
|
||||||
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
|
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
|
||||||
@ -221,13 +163,18 @@ public function deleteTransaction(CashTransaction $transaction): bool
|
|||||||
|
|
||||||
$cashAccount->update(['balance' => $newBalance]);
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
|
||||||
|
$media = $transaction->getFirstMedia('receipts');
|
||||||
|
if ($media) {
|
||||||
|
Cache::forget("cash_transaction_receipt_{$media->id}");
|
||||||
|
}
|
||||||
|
|
||||||
$transaction->clearMediaCollection('receipts');
|
$transaction->clearMediaCollection('receipts');
|
||||||
|
|
||||||
$deleted = $transaction->delete();
|
$deleted = $transaction->delete();
|
||||||
|
|
||||||
if ($deleted) {
|
if ($deleted) {
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Transaksi Kas Dihapus',
|
title: 'Transaksi Kas Dihapus',
|
||||||
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.cash-accounts.index'),
|
url: route('admin.finance.cash-accounts.index'),
|
||||||
@ -237,4 +184,27 @@ public function deleteTransaction(CashTransaction $transaction): bool
|
|||||||
return $deleted;
|
return $deleted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function formatTransaction(CashTransaction $transaction): array
|
||||||
|
{
|
||||||
|
$media = $transaction->getFirstMedia('receipts');
|
||||||
|
|
||||||
|
if (! $media) {
|
||||||
|
return $transaction->toArray() + [
|
||||||
|
'receipt_key' => null,
|
||||||
|
'receipt_url' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$s3Key = $media->file_name;
|
||||||
|
|
||||||
|
if (str_starts_with($s3Key, '/') || ! str_contains($s3Key, '/')) {
|
||||||
|
$s3Key = $media->getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transaction->toArray() + [
|
||||||
|
'receipt_key' => $s3Key,
|
||||||
|
'receipt_url' => $this->s3Service->getTemporaryUrl($s3Key),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,48 +2,35 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
use App\Enums\CashTransactionType;
|
use App\Enums\CashTransactionType;
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Models\EmployeeAdvancePayment;
|
use App\Models\EmployeeAdvancePayment;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
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\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class EmployeeAdvanceService
|
class EmployeeAdvanceService
|
||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions, HasRoleChecks;
|
||||||
|
|
||||||
private function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
|
||||||
->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
|
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->latest()
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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'])
|
||||||
->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
|
->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||||
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): EmployeeAdvance
|
public function store(array $data): EmployeeAdvance
|
||||||
{
|
{
|
||||||
$employee = auth()->user()->employee;
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
@ -64,7 +51,7 @@ public function create(array $data): EmployeeAdvance
|
|||||||
$employeeAdvance->load('employee.user');
|
$employeeAdvance->load('employee.user');
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Kasbon Baru',
|
title: 'Kasbon Baru',
|
||||||
body: 'Kasbon sebesar Rp '.number_format($data['amount'], 0, ',', '.')." dari {$employeeAdvance->employee->name} menunggu persetujuan".' oleh '.auth()->user()->full_name.'.',
|
body: 'Kasbon sebesar Rp '.number_format($data['amount'], 0, ',', '.')." dari {$employeeAdvance->employee->name} menunggu persetujuan".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.employee-advances.index'),
|
url: route('admin.finance.employee-advances.index'),
|
||||||
@ -123,7 +110,7 @@ public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeA
|
|||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(EmployeeAdvance $employeeAdvance): bool
|
public function destroy(EmployeeAdvance $employeeAdvance): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($employeeAdvance) {
|
return DB::transaction(function () use ($employeeAdvance) {
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
||||||
@ -181,7 +168,7 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Kasbon Disetujui',
|
title: 'Kasbon Disetujui',
|
||||||
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah disetujui'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah disetujui'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.employee-advances.index'),
|
url: route('admin.finance.employee-advances.index'),
|
||||||
@ -234,7 +221,7 @@ public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdva
|
|||||||
: 'Pembayaran kasbon sebesar Rp '.number_format($amount, 0, ',', '.').' oleh '.auth()->user()->full_name.'. Sisa: Rp '.number_format($employeeAdvance->amount - $employeeAdvance->paid_amount, 0, ',', '.').'.';
|
: 'Pembayaran kasbon sebesar Rp '.number_format($amount, 0, ',', '.').' oleh '.auth()->user()->full_name.'. Sisa: Rp '.number_format($employeeAdvance->amount - $employeeAdvance->paid_amount, 0, ',', '.').'.';
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: $employeeAdvance->status === EmployeeAdvanceStatus::PAID ? 'Kasbon Dibayar' : 'Pembayaran Kasbon',
|
title: $employeeAdvance->status === EmployeeAdvanceStatus::PAID ? 'Kasbon Dibayar' : 'Pembayaran Kasbon',
|
||||||
body: $notificationBody,
|
body: $notificationBody,
|
||||||
url: route('admin.finance.employee-advances.index'),
|
url: route('admin.finance.employee-advances.index'),
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
use App\Enums\CashTransactionType;
|
use App\Enums\CashTransactionType;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\CashAccount;
|
use App\Models\CashAccount;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
@ -10,7 +11,6 @@
|
|||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@ -23,15 +23,6 @@ public function __construct(
|
|||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return Expense::select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
|
||||||
->with('createdBy.userProfile', 'media')
|
|
||||||
->latest()
|
|
||||||
->get()
|
|
||||||
->map(fn (Expense $expense) => $this->formatExpense($expense));
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
$paginator = Expense::query()
|
$paginator = Expense::query()
|
||||||
@ -46,36 +37,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatExpense(Expense $expense): array
|
public function store(array $data): Expense
|
||||||
{
|
|
||||||
$media = $expense->getFirstMedia('receipts');
|
|
||||||
|
|
||||||
if (! $media) {
|
|
||||||
return $expense->toArray() + [
|
|
||||||
'receipt_key' => null,
|
|
||||||
'receipt_url' => null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$s3Key = $media->file_name;
|
|
||||||
|
|
||||||
// Handle old data where file_name is just the filename, not full path
|
|
||||||
if (str_starts_with($s3Key, '/') || ! str_contains($s3Key, '/')) {
|
|
||||||
$s3Key = $media->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheKey = "expense_receipt_{$media->id}";
|
|
||||||
$receiptUrl = Cache::remember($cacheKey, now()->addMinutes(55), function () use ($s3Key) {
|
|
||||||
return $this->s3Service->getTemporaryUrl($s3Key);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $expense->toArray() + [
|
|
||||||
'receipt_key' => $s3Key,
|
|
||||||
'receipt_url' => $receiptUrl,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(array $data): Expense
|
|
||||||
{
|
{
|
||||||
$expense = DB::transaction(function () use ($data) {
|
$expense = DB::transaction(function () use ($data) {
|
||||||
$cashTransaction = $this->debitCash(
|
$cashTransaction = $this->debitCash(
|
||||||
@ -90,15 +52,15 @@ public function create(array $data): Expense
|
|||||||
'description' => $data['description'],
|
'description' => $data['description'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! empty($data['receipt_key'])) {
|
if (array_key_exists('receipt_key', $data)) {
|
||||||
$this->registerMedia($expense, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
$this->syncReceipt($expense, $data['receipt_key'] ?? null, 'receipts', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $expense;
|
return $expense;
|
||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Pengeluaran Baru',
|
title: 'Pengeluaran Baru',
|
||||||
body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.expenses.index'),
|
url: route('admin.finance.expenses.index'),
|
||||||
@ -138,33 +100,14 @@ public function update(Expense $expense, array $data): Expense
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (array_key_exists('receipt_key', $data)) {
|
if (array_key_exists('receipt_key', $data)) {
|
||||||
$currentMedia = $expense->getFirstMedia('receipts');
|
$this->syncReceipt($expense, $data['receipt_key'] ?? null, 'receipts', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||||
$currentKey = $currentMedia?->file_name;
|
|
||||||
|
|
||||||
// Normalize: if file_name is just a filename (old data), use getPath()
|
|
||||||
if ($currentMedia && ! str_contains($currentKey, '/')) {
|
|
||||||
$currentKey = $currentMedia->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['receipt_key'] !== $currentKey) {
|
|
||||||
// Invalidate old receipt cache
|
|
||||||
if ($currentMedia) {
|
|
||||||
Cache::forget("expense_receipt_{$currentMedia->id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$expense->clearMediaCollection('receipts');
|
|
||||||
|
|
||||||
if (! empty($data['receipt_key'])) {
|
|
||||||
$this->registerMedia($expense, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $expense;
|
return $expense;
|
||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Pengeluaran Diperbarui',
|
title: 'Pengeluaran Diperbarui',
|
||||||
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.expenses.index'),
|
url: route('admin.finance.expenses.index'),
|
||||||
@ -173,7 +116,7 @@ public function update(Expense $expense, array $data): Expense
|
|||||||
return $expense;
|
return $expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Expense $expense): bool
|
public function destroy(Expense $expense): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($expense) {
|
return DB::transaction(function () use ($expense) {
|
||||||
$this->creditCash(
|
$this->creditCash(
|
||||||
@ -182,7 +125,6 @@ public function delete(Expense $expense): bool
|
|||||||
type: CashTransactionType::DEPOSIT,
|
type: CashTransactionType::DEPOSIT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invalidate receipt cache
|
|
||||||
$media = $expense->getFirstMedia('receipts');
|
$media = $expense->getFirstMedia('receipts');
|
||||||
if ($media) {
|
if ($media) {
|
||||||
Cache::forget("expense_receipt_{$media->id}");
|
Cache::forget("expense_receipt_{$media->id}");
|
||||||
@ -194,7 +136,7 @@ public function delete(Expense $expense): bool
|
|||||||
|
|
||||||
if ($deleted) {
|
if ($deleted) {
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Pengeluaran Dihapus',
|
title: 'Pengeluaran Dihapus',
|
||||||
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.expenses.index'),
|
url: route('admin.finance.expenses.index'),
|
||||||
@ -204,4 +146,32 @@ public function delete(Expense $expense): bool
|
|||||||
return $deleted;
|
return $deleted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function formatExpense(Expense $expense): array
|
||||||
|
{
|
||||||
|
$media = $expense->getFirstMedia('receipts');
|
||||||
|
|
||||||
|
if (! $media) {
|
||||||
|
return $expense->toArray() + [
|
||||||
|
'receipt_key' => null,
|
||||||
|
'receipt_url' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$s3Key = $media->file_name;
|
||||||
|
|
||||||
|
if (str_starts_with($s3Key, '/') || ! str_contains($s3Key, '/')) {
|
||||||
|
$s3Key = $media->getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = "expense_receipt_{$media->id}";
|
||||||
|
$receiptUrl = Cache::remember($cacheKey, now()->addMinutes(55), function () use ($s3Key) {
|
||||||
|
return $this->s3Service->getTemporaryUrl($s3Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $expense->toArray() + [
|
||||||
|
'receipt_key' => $s3Key,
|
||||||
|
'receipt_url' => $receiptUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance\Payroll;
|
||||||
|
|
||||||
use App\Enums\PayrollAdjustmentType;
|
use App\Enums\PayrollAdjustmentType;
|
||||||
use App\Enums\PayrollStatus;
|
use App\Enums\PayrollStatus;
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
class PayrollAdjustmentService
|
class PayrollAdjustmentService
|
||||||
{
|
{
|
||||||
public function create(Payroll $payroll, array $data): PayrollAdjustment
|
public function store(Payroll $payroll, array $data): PayrollAdjustment
|
||||||
{
|
{
|
||||||
if ($payroll->status !== PayrollStatus::UNPAID) {
|
if ($payroll->status !== PayrollStatus::UNPAID) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@ -35,7 +35,7 @@ public function create(Payroll $payroll, array $data): PayrollAdjustment
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(PayrollAdjustment $adjustment): bool
|
public function destroy(PayrollAdjustment $adjustment): bool
|
||||||
{
|
{
|
||||||
$payroll = $adjustment->payroll;
|
$payroll = $adjustment->payroll;
|
||||||
|
|
||||||
@ -1,57 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance\Payroll;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
use App\Enums\CashTransactionType;
|
use App\Enums\CashTransactionType;
|
||||||
use App\Enums\PayrollPeriodStatus;
|
use App\Enums\PayrollPeriodStatus;
|
||||||
use App\Enums\PayrollStatus;
|
use App\Enums\PayrollStatus;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Models\PayrollPeriod;
|
use App\Models\PayrollPeriod;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
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\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class PayrollPeriodService
|
class PayrollPeriodService
|
||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions, HasRoleChecks;
|
||||||
|
|
||||||
private function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return PayrollPeriod::select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
|
||||||
->when(! $this->canViewAll(), function ($query) {
|
|
||||||
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_deduction_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'deduction_amount')
|
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
|
|
||||||
})
|
|
||||||
->when($this->canViewAll(), function ($query) {
|
|
||||||
$query->withCount('payrolls')
|
|
||||||
->withSum('payrolls', 'total_amount')
|
|
||||||
->withSum('payrolls', 'bonus_amount')
|
|
||||||
->withSum('payrolls', 'deduction_amount')
|
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()])
|
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()]);
|
|
||||||
})
|
|
||||||
->latest('year')
|
|
||||||
->latest('month')
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PayrollPeriod::query()
|
return PayrollPeriod::query()
|
||||||
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
||||||
->when(! $this->canViewAll(), function ($query) {
|
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), function ($query) {
|
||||||
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
||||||
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
|
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
|
||||||
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
|
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
|
||||||
@ -59,7 +31,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
|
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
|
||||||
})
|
})
|
||||||
->when($this->canViewAll(), function ($query) {
|
->when(self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), function ($query) {
|
||||||
$query->withCount('payrolls')
|
$query->withCount('payrolls')
|
||||||
->withSum('payrolls', 'total_amount')
|
->withSum('payrolls', 'total_amount')
|
||||||
->withSum('payrolls', 'bonus_amount')
|
->withSum('payrolls', 'bonus_amount')
|
||||||
@ -72,15 +44,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDetail(PayrollPeriod $period): PayrollPeriod
|
public function getCurrentOrCreate(): PayrollPeriod
|
||||||
{
|
{
|
||||||
return $period->load([
|
$now = now();
|
||||||
'payrolls' => function ($query) {
|
|
||||||
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
|
return PayrollPeriod::firstOrCreate(
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
['year' => $now->year, 'month' => $now->month],
|
||||||
->orderBy('id');
|
['status' => PayrollPeriodStatus::OPEN]
|
||||||
},
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function close(PayrollPeriod $period): PayrollPeriod
|
public function close(PayrollPeriod $period): PayrollPeriod
|
||||||
@ -161,7 +132,7 @@ public function pay(Payroll $payroll): Payroll
|
|||||||
$employeeUser = $payroll->employee->user ?? null;
|
$employeeUser = $payroll->employee->user ?? null;
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Gaji Dibayar',
|
title: 'Gaji Dibayar',
|
||||||
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibayar".' oleh '.auth()->user()->full_name.'.',
|
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibayar".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.payroll-periods.index'),
|
url: route('admin.finance.payroll-periods.index'),
|
||||||
@ -192,7 +163,7 @@ public function cancel(Payroll $payroll): Payroll
|
|||||||
$employeeUser = $payroll->employee->user ?? null;
|
$employeeUser = $payroll->employee->user ?? null;
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Gaji Dibatalkan',
|
title: 'Gaji Dibatalkan',
|
||||||
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibatalkan".' oleh '.auth()->user()->full_name.'.',
|
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibatalkan".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.payroll-periods.index'),
|
url: route('admin.finance.payroll-periods.index'),
|
||||||
@ -2,30 +2,54 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\HR;
|
namespace App\Services\Admin\HR;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
|
use App\Models\User;
|
||||||
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;
|
||||||
|
use App\Settings\HRSettings;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class AttendanceService
|
class AttendanceService
|
||||||
{
|
{
|
||||||
use RegistersMedia;
|
use HasRoleChecks, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
|
private EmployeeService $employeeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(): Collection
|
public function getIndexData(int $year, int $month): array
|
||||||
{
|
{
|
||||||
return Attendance::with(['employee.user.userProfile', 'media'])
|
$user = auth()->user();
|
||||||
->latest('attendance_date')
|
$isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER]);
|
||||||
->get()
|
$hrSettings = app(HRSettings::class);
|
||||||
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
|
||||||
|
$employeeId = $isAdmin ? null : $user->employee?->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'attendances' => $this->getByMonth($year, $month, $employeeId),
|
||||||
|
'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
|
||||||
|
'employees' => $isAdmin ? $this->employeeService->getAll() : [],
|
||||||
|
'todayAttendance' => $isAdmin ? null : $this->getToday(),
|
||||||
|
'currentYear' => $year,
|
||||||
|
'currentMonth' => $month,
|
||||||
|
'monthStats' => $this->getMonthStats($year, $month, $employeeId),
|
||||||
|
'hrSettings' => [
|
||||||
|
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
|
||||||
|
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
|
||||||
|
],
|
||||||
|
'isOnLeave' => $isAdmin ? false : $this->isOnLeave($user),
|
||||||
|
'canCheckIn' => $isAdmin ? false : $user->employee !== null,
|
||||||
|
'isAdmin' => $isAdmin,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
||||||
@ -53,55 +77,64 @@ public function getToday(): ?array
|
|||||||
return $this->getByDate(now()->toDateString());
|
return $this->getByDate(now()->toDateString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAllEmployees(): Collection
|
public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
||||||
{
|
{
|
||||||
return Employee::with(['user.userProfile', 'user.roles'])
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||||
->whereHas('user', fn ($q) => $q->where('is_active', true))
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
||||||
->get();
|
|
||||||
|
return LeaveRequest::approved()
|
||||||
|
->with('employee.user.userProfile')
|
||||||
|
->where('start_date', '<=', $endOfMonth)
|
||||||
|
->where('end_date', '>=', $startOfMonth)
|
||||||
|
->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
|
||||||
|
->get()
|
||||||
|
->map(fn (LeaveRequest $leave) => [
|
||||||
|
'id' => $leave->id,
|
||||||
|
'employee_id' => $leave->employee_id,
|
||||||
|
'start_date' => $leave->start_date->toDateString(),
|
||||||
|
'end_date' => $leave->end_date->toDateString(),
|
||||||
|
'total_days' => $leave->total_days,
|
||||||
|
'status' => $leave->status->value,
|
||||||
|
'employee_name' => $leave->employee?->user?->userProfile?->full_name ?? '-',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMonthStats(int $year, int $month, ?int $employeeId = null): array
|
public function getMonthStats(int $year, int $month, ?int $employeeId = null): array
|
||||||
{
|
{
|
||||||
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||||
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
$endOfMonth = $startOfMonth->copy()->endOfMonth();
|
||||||
|
$today = Carbon::today();
|
||||||
|
$statEnd = $endOfMonth->lte($today) ? $endOfMonth : $today;
|
||||||
|
|
||||||
$workingDays = 0;
|
$workingDays = 0;
|
||||||
$current = $startOfMonth->copy();
|
$current = $startOfMonth->copy();
|
||||||
while ($current->lte($endOfMonth)) {
|
while ($current->lte($statEnd)) {
|
||||||
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
||||||
$workingDays++;
|
$workingDays++;
|
||||||
}
|
}
|
||||||
$current->addDay();
|
$current->addDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
$attendanceQuery = Attendance::whereYear('attendance_date', $year)
|
|
||||||
->whereMonth('attendance_date', $month);
|
|
||||||
if ($employeeId) {
|
if ($employeeId) {
|
||||||
$attendanceQuery->where('employee_id', $employeeId);
|
return $this->getMonthStatsForEmployee($year, $month, $startOfMonth, $statEnd, $workingDays, $employeeId);
|
||||||
}
|
}
|
||||||
$attendanceCount = $attendanceQuery->count();
|
|
||||||
|
|
||||||
$leaveQuery = LeaveRequest::approved()
|
return $this->getMonthStatsForAll($year, $month, $startOfMonth, $statEnd, $workingDays);
|
||||||
->where('start_date', '<=', $endOfMonth)
|
}
|
||||||
->where('end_date', '>=', $startOfMonth);
|
|
||||||
if ($employeeId) {
|
public function isOnLeave(User $user): bool
|
||||||
$leaveQuery->where('employee_id', $employeeId);
|
{
|
||||||
|
$employee = $user->employee;
|
||||||
|
|
||||||
|
if (! $employee) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
$leaveDays = $leaveQuery->get()
|
|
||||||
->reduce(function ($carry, $leave) use ($startOfMonth, $endOfMonth) {
|
|
||||||
$leaveStart = max($leave->start_date->timestamp, $startOfMonth->timestamp);
|
|
||||||
$leaveEnd = min($leave->end_date->timestamp, $endOfMonth->timestamp);
|
|
||||||
$days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1;
|
|
||||||
|
|
||||||
return $carry + max(0, $days);
|
return LeaveRequest::approved()
|
||||||
}, 0);
|
->where('employee_id', $employee->id)
|
||||||
|
->where('start_date', '<=', now()->toDateString())
|
||||||
return [
|
->where('end_date', '>=', now()->toDateString())
|
||||||
'working_days' => $workingDays,
|
->exists();
|
||||||
'present' => $attendanceCount,
|
|
||||||
'absent' => max(0, $workingDays - $attendanceCount - $leaveDays),
|
|
||||||
'leave' => $leaveDays,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function checkIn(array $data): Attendance
|
public function checkIn(array $data): Attendance
|
||||||
@ -109,7 +142,9 @@ public function checkIn(array $data): Attendance
|
|||||||
$employee = auth()->user()->employee;
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
if (! $employee) {
|
if (! $employee) {
|
||||||
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
throw ValidationException::withMessages([
|
||||||
|
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$today = now()->toDateString();
|
$today = now()->toDateString();
|
||||||
@ -119,7 +154,9 @@ public function checkIn(array $data): Attendance
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
throw new \Exception('Anda sudah melakukan presensi hari ini.');
|
throw ValidationException::withMessages([
|
||||||
|
'attendance' => 'Anda sudah melakukan presensi hari ini.',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$attendance = Attendance::create([
|
$attendance = Attendance::create([
|
||||||
@ -135,7 +172,7 @@ public function checkIn(array $data): Attendance
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||||
title: 'Presensi Masuk',
|
title: 'Presensi Masuk',
|
||||||
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
|
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.hr.attendances.index'),
|
url: route('admin.hr.attendances.index'),
|
||||||
@ -157,7 +194,7 @@ public function checkOut(Attendance $attendance, array $data): Attendance
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||||
title: 'Presensi Pulang',
|
title: 'Presensi Pulang',
|
||||||
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
|
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.hr.attendances.index'),
|
url: route('admin.hr.attendances.index'),
|
||||||
@ -166,6 +203,93 @@ public function checkOut(Attendance $attendance, array $data): Attendance
|
|||||||
return $attendance;
|
return $attendance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays, int $employeeId): array
|
||||||
|
{
|
||||||
|
$attendanceQuery = Attendance::whereYear('attendance_date', $year)
|
||||||
|
->whereMonth('attendance_date', $month)
|
||||||
|
->where('attendance_date', '<=', $statEnd->toDateString())
|
||||||
|
->where('employee_id', $employeeId);
|
||||||
|
|
||||||
|
$attendanceDates = $attendanceQuery->pluck('attendance_date')
|
||||||
|
->map(fn ($d) => Carbon::parse($d)->toDateString())
|
||||||
|
->filter(fn ($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]))
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
$attendanceCount = $attendanceDates->count();
|
||||||
|
|
||||||
|
$leaveQuery = LeaveRequest::approved()
|
||||||
|
->where('start_date', '<=', $statEnd)
|
||||||
|
->where('end_date', '>=', $startOfMonth)
|
||||||
|
->where('employee_id', $employeeId);
|
||||||
|
|
||||||
|
$leaveRequests = $leaveQuery->get();
|
||||||
|
$leaveCount = $leaveRequests->count();
|
||||||
|
|
||||||
|
$leaveDates = collect();
|
||||||
|
$leaveRequests->each(function ($leave) use (&$leaveDates, $startOfMonth, $statEnd) {
|
||||||
|
$leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
|
||||||
|
$leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
|
||||||
|
$current = $leaveStart->copy();
|
||||||
|
while ($current->lte($leaveEnd)) {
|
||||||
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
||||||
|
$leaveDates->push($current->toDateString());
|
||||||
|
}
|
||||||
|
$current->addDay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$leaveDates = $leaveDates->unique()->values();
|
||||||
|
|
||||||
|
$coveredDates = $attendanceDates->merge($leaveDates)->unique()->count();
|
||||||
|
$absent = max(0, $workingDays - $coveredDates);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'working_days' => $workingDays,
|
||||||
|
'present' => $attendanceCount,
|
||||||
|
'absent' => $absent,
|
||||||
|
'leave' => $leaveCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMonthStatsForAll(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays): array
|
||||||
|
{
|
||||||
|
$totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count();
|
||||||
|
|
||||||
|
$presentCount = Attendance::whereYear('attendance_date', $year)
|
||||||
|
->whereMonth('attendance_date', $month)
|
||||||
|
->where('attendance_date', '<=', $statEnd->toDateString())
|
||||||
|
->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$leaveRequests = LeaveRequest::approved()
|
||||||
|
->where('start_date', '<=', $statEnd)
|
||||||
|
->where('end_date', '>=', $startOfMonth)
|
||||||
|
->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true)))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$leaveDays = 0;
|
||||||
|
$leaveRequests->each(function ($leave) use (&$leaveDays, $startOfMonth, $statEnd) {
|
||||||
|
$leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay();
|
||||||
|
$leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay();
|
||||||
|
$current = $leaveStart->copy();
|
||||||
|
while ($current->lte($leaveEnd)) {
|
||||||
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
||||||
|
$leaveDays++;
|
||||||
|
}
|
||||||
|
$current->addDay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$absent = max(0, ($totalEmployees * $workingDays) - $presentCount - $leaveDays);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'working_days' => $workingDays,
|
||||||
|
'present' => $presentCount,
|
||||||
|
'absent' => $absent,
|
||||||
|
'leave' => $leaveDays,
|
||||||
|
'total_employees' => $totalEmployees,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function formatAttendance(Attendance $attendance): array
|
private function formatAttendance(Attendance $attendance): array
|
||||||
{
|
{
|
||||||
$toArray = $attendance->toArray();
|
$toArray = $attendance->toArray();
|
||||||
|
|||||||
@ -2,46 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\HR;
|
namespace App\Services\Admin\HR;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
|
use App\Models\Employee;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class EmployeeService
|
class EmployeeService
|
||||||
{
|
{
|
||||||
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
|
use HasRoleChecks;
|
||||||
private const RESTRICTED_ROLES = ['admin-toko', 'direktur'];
|
|
||||||
|
|
||||||
public function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(self::ADMIN_ROLES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function shouldHideAdminBahanBaku(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(self::RESTRICTED_ROLES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return User::select(['id', 'email', 'username', 'is_active'])
|
|
||||||
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
|
||||||
->with([
|
|
||||||
'userProfile' => fn ($q) => $q->select(['id', 'user_id', 'full_name', 'phone_number', 'gender']),
|
|
||||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
|
||||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
|
||||||
])
|
|
||||||
->when(! $this->canViewAll(), function ($q) {
|
|
||||||
$userRoles = auth()->user()->roles->pluck('name');
|
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
|
||||||
})
|
|
||||||
->when($this->shouldHideAdminBahanBaku(), fn ($q) => $q->whereDoesntHave('roles', fn ($rq) => $rq->where('name', 'admin-bahan-baku')))
|
|
||||||
->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($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
|
||||||
->latest()
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
@ -53,11 +24,11 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
'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']),
|
'roles' => fn ($q) => $q->select(['id', 'name']),
|
||||||
])
|
])
|
||||||
->when(! $this->canViewAll(), function ($q) {
|
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), function ($q) {
|
||||||
$userRoles = auth()->user()->roles->pluck('name');
|
$userRoles = auth()->user()->roles->pluck('name');
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
||||||
})
|
})
|
||||||
->when($this->shouldHideAdminBahanBaku(), fn ($q) => $q->whereDoesntHave('roles', fn ($rq) => $rq->where('name', 'admin-bahan-baku')))
|
->when(self::hasAnyRole([Role::ADMIN_TOKO, Role::DIREKTUR]), fn ($q) => $q->whereDoesntHave('roles', fn ($rq) => $rq->where('name', Role::ADMIN_BAHAN_BAKU->value)))
|
||||||
->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)))
|
||||||
@ -66,7 +37,18 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): User
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return Employee::with(['user.userProfile', 'user.roles'])
|
||||||
|
->whereHas('user', fn ($q) => $q->where('is_active', true))
|
||||||
|
->get()
|
||||||
|
->map(fn (Employee $employee) => [
|
||||||
|
'id' => $employee->id,
|
||||||
|
'name' => $employee->user?->userProfile?->full_name ?? '-',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(array $data): User
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
@ -132,7 +114,7 @@ public function update(User $user, array $data): User
|
|||||||
return $user->fresh(['userProfile', 'employee']);
|
return $user->fresh(['userProfile', 'employee']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(User $user): bool
|
public function destroy(User $user): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($user) {
|
return DB::transaction(function () use ($user) {
|
||||||
$user->employee()->delete();
|
$user->employee()->delete();
|
||||||
|
|||||||
@ -2,39 +2,26 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\HR;
|
namespace App\Services\Admin\HR;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
use App\Enums\LeaveRequestStatus;
|
use App\Enums\LeaveRequestStatus;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class LeaveRequestService
|
class LeaveRequestService
|
||||||
{
|
{
|
||||||
private function canViewAll(): bool
|
use HasRoleChecks;
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return LeaveRequest::select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
|
||||||
->with(['employee.user.userProfile'])
|
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->when($filters['status'] ?? null, function ($query, $status) {
|
|
||||||
$query->where('status', $status);
|
|
||||||
})
|
|
||||||
->latest()
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 LeaveRequest::query()
|
return LeaveRequest::query()
|
||||||
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
->when(! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO]), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||||
->when($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
->when($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||||
->when($filters['status'] ?? null, function ($query, $status) {
|
->when($filters['status'] ?? null, function ($query, $status) {
|
||||||
$query->where('status', $status);
|
$query->where('status', $status);
|
||||||
@ -43,13 +30,15 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): LeaveRequest
|
public function store(array $data): LeaveRequest
|
||||||
{
|
{
|
||||||
$leaveRequest = DB::transaction(function () use ($data) {
|
$leaveRequest = DB::transaction(function () use ($data) {
|
||||||
$employee = auth()->user()->employee;
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
if (! $employee) {
|
if (! $employee) {
|
||||||
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
throw ValidationException::withMessages([
|
||||||
|
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$startDate = new Carbon($data['start_date']);
|
$startDate = new Carbon($data['start_date']);
|
||||||
@ -68,7 +57,7 @@ public function create(array $data): LeaveRequest
|
|||||||
$leaveRequest->load('employee.user');
|
$leaveRequest->load('employee.user');
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Pengajuan Cuti Baru',
|
title: 'Pengajuan Cuti Baru',
|
||||||
body: "Pengajuan cuti {$leaveRequest->total_days} hari oleh ".auth()->user()->full_name.'.',
|
body: "Pengajuan cuti {$leaveRequest->total_days} hari oleh ".auth()->user()->full_name.'.',
|
||||||
url: route('admin.hr.leave-requests.index'),
|
url: route('admin.hr.leave-requests.index'),
|
||||||
@ -95,7 +84,7 @@ public function update(LeaveRequest $leaveRequest, array $data): LeaveRequest
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(LeaveRequest $leaveRequest): bool
|
public function destroy(LeaveRequest $leaveRequest): bool
|
||||||
{
|
{
|
||||||
return $leaveRequest->delete();
|
return $leaveRequest->delete();
|
||||||
}
|
}
|
||||||
@ -111,7 +100,7 @@ public function approve(LeaveRequest $leaveRequest): LeaveRequest
|
|||||||
$employeeUser = $leaveRequest->employee->user ?? null;
|
$employeeUser = $leaveRequest->employee->user ?? null;
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Cuti Disetujui',
|
title: 'Cuti Disetujui',
|
||||||
body: "Cuti {$leaveRequest->employee->name} telah disetujui oleh ".auth()->user()->full_name.'.',
|
body: "Cuti {$leaveRequest->employee->name} telah disetujui oleh ".auth()->user()->full_name.'.',
|
||||||
url: route('admin.hr.leave-requests.index'),
|
url: route('admin.hr.leave-requests.index'),
|
||||||
@ -132,7 +121,7 @@ public function reject(LeaveRequest $leaveRequest): LeaveRequest
|
|||||||
$employeeUser = $leaveRequest->employee->user ?? null;
|
$employeeUser = $leaveRequest->employee->user ?? null;
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Cuti Ditolak',
|
title: 'Cuti Ditolak',
|
||||||
body: "Cuti {$leaveRequest->employee->name} telah ditolak oleh ".auth()->user()->full_name.'.',
|
body: "Cuti {$leaveRequest->employee->name} telah ditolak oleh ".auth()->user()->full_name.'.',
|
||||||
url: route('admin.hr.leave-requests.index'),
|
url: route('admin.hr.leave-requests.index'),
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
use App\Models\CuttingMaterial;
|
use App\Models\CuttingMaterial;
|
||||||
use App\Models\CuttingMaterialCombination;
|
use App\Models\CuttingMaterialCombination;
|
||||||
use App\Models\CuttingResult;
|
use App\Models\CuttingResult;
|
||||||
use App\Models\RawMaterial;
|
|
||||||
use App\Models\RawMaterialPrice;
|
use App\Models\RawMaterialPrice;
|
||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
@ -61,28 +60,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getForCreate(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'rawMaterials' => RawMaterial::query()
|
|
||||||
->select(['id', 'name', 'unit', 'is_active'])
|
|
||||||
->with([
|
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
|
||||||
])
|
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
|
||||||
->get()
|
|
||||||
->each(function (RawMaterial $rawMaterial) {
|
|
||||||
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
|
|
||||||
$media = $price->getFirstMedia('photos');
|
|
||||||
$price->photo_url = $media
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
||||||
: null;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getForEdit(Cutting $cutting): array
|
public function getForEdit(Cutting $cutting): array
|
||||||
{
|
{
|
||||||
$cutting->load([
|
$cutting->load([
|
||||||
@ -143,7 +120,7 @@ public function getForEdit(Cutting $cutting): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Cutting
|
public function store(array $data): Cutting
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
foreach ($data['materials'] as $materialData) {
|
foreach ($data['materials'] as $materialData) {
|
||||||
@ -349,7 +326,7 @@ public function update(Cutting $cutting, array $data): Cutting
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Cutting $cutting): bool
|
public function destroy(Cutting $cutting): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($cutting) {
|
return DB::transaction(function () use ($cutting) {
|
||||||
$cutting->load('cuttingMaterials.rawMaterialPrice');
|
$cutting->load('cuttingMaterials.rawMaterialPrice');
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Manage;
|
namespace App\Services\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\PurchaseItem;
|
use App\Models\PurchaseItem;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
@ -154,16 +155,16 @@ public function getForEdit(Purchase $purchase): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Purchase
|
public function store(array $data): Purchase
|
||||||
{
|
{
|
||||||
if (($data['mode'] ?? 'new') === 'existing') {
|
if (($data['mode'] ?? 'new') === 'existing') {
|
||||||
return $this->createFromExisting($data);
|
return $this->storeFromExisting($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->createNew($data);
|
return $this->storeNew($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createFromExisting(array $data): Purchase
|
private function storeFromExisting(array $data): Purchase
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$now = now();
|
$now = now();
|
||||||
@ -219,7 +220,7 @@ private function createFromExisting(array $data): Purchase
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Bahan Baku'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||||
title: 'Belanja Baru',
|
title: 'Belanja Baru',
|
||||||
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.manage.purchases.index'),
|
url: route('admin.manage.purchases.index'),
|
||||||
@ -229,7 +230,7 @@ private function createFromExisting(array $data): Purchase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createNew(array $data): Purchase
|
private function storeNew(array $data): Purchase
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$rawMaterial = RawMaterial::create([
|
$rawMaterial = RawMaterial::create([
|
||||||
@ -309,7 +310,7 @@ private function createNew(array $data): Purchase
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Bahan Baku'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||||
title: 'Belanja Baru',
|
title: 'Belanja Baru',
|
||||||
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.manage.purchases.index'),
|
url: route('admin.manage.purchases.index'),
|
||||||
@ -334,12 +335,6 @@ public function update(Purchase $purchase, array $data): Purchase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$oldMaterial = $oldItems
|
|
||||||
->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
|
|
||||||
->filter()
|
|
||||||
->unique(fn (RawMaterial $material) => $material->id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$oldMaterial = $oldItems
|
$oldMaterial = $oldItems
|
||||||
->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
|
->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
|
||||||
->filter()
|
->filter()
|
||||||
@ -466,7 +461,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Purchase $purchase): bool
|
public function destroy(Purchase $purchase): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($purchase) {
|
return DB::transaction(function () use ($purchase) {
|
||||||
$purchase->load('purchaseItems.rawMaterialPrice');
|
$purchase->load('purchaseItems.rawMaterialPrice');
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
use App\Models\Product;
|
use App\Enums\Role;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use App\Models\Restock;
|
use App\Models\Restock;
|
||||||
use App\Models\RestockItem;
|
use App\Models\RestockItem;
|
||||||
@ -59,65 +59,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getForCreate(): array
|
public function store(array $data): Restock
|
||||||
{
|
|
||||||
return [
|
|
||||||
'products' => Product::query()
|
|
||||||
->select(['id', 'name', 'status'])
|
|
||||||
->with([
|
|
||||||
'productVariants:id,product_id,name,stock,reject_stock',
|
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
|
||||||
])
|
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
|
||||||
->get()
|
|
||||||
->each(function (Product $product) {
|
|
||||||
$product->productVariants->each(function (ProductVariant $variant) {
|
|
||||||
$media = $variant->getFirstMedia('photos');
|
|
||||||
$variant->photo_url = $media
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
$capitalPrice = $variant->productPrices
|
|
||||||
->first(fn ($price) => $price->type === PriceType::CAPITAL);
|
|
||||||
$variant->capital_price = $capitalPrice?->price ?? 0;
|
|
||||||
|
|
||||||
$rejectPrice = $variant->productPrices
|
|
||||||
->first(fn ($price) => $price->type === PriceType::REJECT);
|
|
||||||
$variant->reject_price = $rejectPrice?->price ?? 0;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getForEdit(Restock $restock): array
|
|
||||||
{
|
|
||||||
$restock->load([
|
|
||||||
'restockItems' => fn ($q) => $q
|
|
||||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
|
||||||
'restockItems.productVariant.product',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$media = $restock->getFirstMedia('photos');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => $restock->id,
|
|
||||||
'stock_type' => $restock->stock_type->value,
|
|
||||||
'notes' => $restock->notes,
|
|
||||||
'photo_key' => $media?->file_name,
|
|
||||||
'photo_url' => $media
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
||||||
: null,
|
|
||||||
'items' => $restock->restockItems->map(fn (RestockItem $item) => [
|
|
||||||
'id' => $item->id,
|
|
||||||
'product_variant_id' => $item->product_variant_id,
|
|
||||||
'quantity' => $item->quantity,
|
|
||||||
'unit_price' => $item->unit_price,
|
|
||||||
])->values(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(array $data): Restock
|
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$now = now();
|
$now = now();
|
||||||
@ -143,7 +85,7 @@ public function create(array $data): Restock
|
|||||||
$this->syncPhoto($restock, $data);
|
$this->syncPhoto($restock, $data);
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||||
title: 'Restock Baru',
|
title: 'Restock Baru',
|
||||||
body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($subtotal, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($subtotal, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.manage.restocks.index'),
|
url: route('admin.manage.restocks.index'),
|
||||||
@ -189,7 +131,7 @@ public function update(Restock $restock, array $data): Restock
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Restock $restock): bool
|
public function destroy(Restock $restock): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($restock) {
|
return DB::transaction(function () use ($restock) {
|
||||||
$restock->load('restockItems');
|
$restock->load('restockItems');
|
||||||
|
|||||||
@ -7,10 +7,10 @@
|
|||||||
use App\Enums\PaymentType;
|
use App\Enums\PaymentType;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
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\HasStockAdjustment;
|
||||||
@ -41,7 +41,7 @@ public function __construct(
|
|||||||
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
|
||||||
{
|
{
|
||||||
$paginator = Order::query()
|
$paginator = Order::query()
|
||||||
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'tiktok_order_id', 'shopee_order_id', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
||||||
->with([
|
->with([
|
||||||
'createdBy:id',
|
'createdBy:id',
|
||||||
'createdBy.userProfile:id,user_id,full_name',
|
'createdBy.userProfile:id,user_id,full_name',
|
||||||
@ -96,7 +96,7 @@ public function getSummary(array $filters = []): array
|
|||||||
$query = Order::query()
|
$query = Order::query()
|
||||||
->selectRaw('COUNT(*) as total_orders')
|
->selectRaw('COUNT(*) as total_orders')
|
||||||
->selectRaw('COALESCE(SUM(subtotal), 0) as total_subtotal')
|
->selectRaw('COALESCE(SUM(subtotal), 0) as total_subtotal')
|
||||||
->selectRaw('COALESCE(SUM(subtotal) - SUM(COALESCE(nego_price, subtotal)), 0) as total_discount')
|
->selectRaw('COALESCE(SUM(discount) + SUM(COALESCE(nego_price, 0)), 0) as total_discount')
|
||||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
||||||
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
||||||
@ -143,83 +143,7 @@ public function getFilterOptions(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getForCreate(): array
|
public function store(array $data): Order
|
||||||
{
|
|
||||||
return [
|
|
||||||
'products' => Product::query()
|
|
||||||
->select(['id', 'name', 'status'])
|
|
||||||
->with([
|
|
||||||
'productVariants:id,product_id,name,stock,reject_stock',
|
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
|
||||||
])
|
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
|
||||||
->get()
|
|
||||||
->each(function (Product $product) {
|
|
||||||
$product->productVariants->each(function (ProductVariant $variant) {
|
|
||||||
$media = $variant->getFirstMedia('photos');
|
|
||||||
$variant->photo_url = $media
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
$prices = $variant->productPrices->mapWithKeys(fn ($p) => [$p->type->value => $p->price]);
|
|
||||||
$variant->prices = $prices;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
'customers' => Customer::query()
|
|
||||||
->select(['id', 'name'])
|
|
||||||
->orderBy('name')
|
|
||||||
->get(),
|
|
||||||
'employees' => User::query()
|
|
||||||
->select('id')
|
|
||||||
->active()
|
|
||||||
->with('userProfile:id,user_id,full_name')
|
|
||||||
->orderBy('id')
|
|
||||||
->get()
|
|
||||||
->filter(fn (User $user) => $user->userProfile?->full_name)
|
|
||||||
->values(),
|
|
||||||
'channelOptions' => OrderChannel::toSelect(),
|
|
||||||
'paymentTypeOptions' => PaymentType::toSelect(),
|
|
||||||
'priceTypeOptions' => PriceType::toSelect()->filter(fn ($p) => $p['value'] !== PriceType::CAPITAL->value)->values(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getForEdit(Order $order): array
|
|
||||||
{
|
|
||||||
$order->load('orderItems.productVariant.product');
|
|
||||||
|
|
||||||
$media = $order->getFirstMedia('photos');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => $order->id,
|
|
||||||
'order_number' => $order->order_number,
|
|
||||||
'stock_type' => $order->orderItems->first()?->stock_type?->value ?? ProductStockQuality::GOOD->value,
|
|
||||||
'channel' => $order->channel?->value ?? OrderChannel::STORE->value,
|
|
||||||
'price_type' => $order->price_type?->value ?? PriceType::RETAIL->value,
|
|
||||||
'payment_type' => $order->payment_type?->value ?? PaymentType::CASH->value,
|
|
||||||
'customer_id' => $order->customer_id,
|
|
||||||
'marketing_id' => $order->marketing_id,
|
|
||||||
'discount' => $order->discount,
|
|
||||||
'nego_price' => $order->nego_price,
|
|
||||||
'is_completed' => $order->status === OrderStatus::COMPLETED,
|
|
||||||
'is_affiliate' => $order->is_affiliate,
|
|
||||||
'tiktok_order_id' => $order->tiktok_order_id,
|
|
||||||
'shopee_order_id' => $order->shopee_order_id,
|
|
||||||
'notes' => $order->notes,
|
|
||||||
'photo_key' => $media?->file_name,
|
|
||||||
'photo_url' => $media
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
||||||
: null,
|
|
||||||
'items' => $order->orderItems->map(fn (OrderItem $item) => [
|
|
||||||
'id' => $item->id,
|
|
||||||
'product_variant_id' => $item->product_variant_id,
|
|
||||||
'quantity' => $item->quantity,
|
|
||||||
'unit_price' => $item->unit_price,
|
|
||||||
])->values(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(array $data): Order
|
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$now = now();
|
$now = now();
|
||||||
@ -234,7 +158,7 @@ public function create(array $data): Order
|
|||||||
|
|
||||||
$discount = (int) ($data['discount'] ?? 0);
|
$discount = (int) ($data['discount'] ?? 0);
|
||||||
$negoPrice = ! empty($data['nego_price']) ? (int) $data['nego_price'] : null;
|
$negoPrice = ! empty($data['nego_price']) ? (int) $data['nego_price'] : null;
|
||||||
$totalAmount = $subtotal - $discount + ($negoPrice ?? 0);
|
$totalAmount = $subtotal - $discount - ($negoPrice ?? 0);
|
||||||
|
|
||||||
$order = Order::create([
|
$order = Order::create([
|
||||||
'created_by_id' => auth()->id(),
|
'created_by_id' => auth()->id(),
|
||||||
@ -268,7 +192,7 @@ public function create(array $data): Order
|
|||||||
}
|
}
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||||
title: 'Transaksi Baru',
|
title: 'Transaksi Baru',
|
||||||
body: 'Transaksi '.$order->order_number.' sebesar Rp '.number_format($totalAmount, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
body: 'Transaksi '.$order->order_number.' sebesar Rp '.number_format($totalAmount, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.manage.transactions.index'),
|
url: route('admin.manage.transactions.index'),
|
||||||
@ -301,7 +225,7 @@ public function update(Order $order, array $data): Order
|
|||||||
|
|
||||||
$discount = (int) ($data['discount'] ?? 0);
|
$discount = (int) ($data['discount'] ?? 0);
|
||||||
$negoPrice = ! empty($data['nego_price']) ? (int) $data['nego_price'] : null;
|
$negoPrice = ! empty($data['nego_price']) ? (int) $data['nego_price'] : null;
|
||||||
$totalAmount = $subtotal - $discount + ($negoPrice ?? 0);
|
$totalAmount = $subtotal - $discount - ($negoPrice ?? 0);
|
||||||
|
|
||||||
foreach ($itemRows as &$row) {
|
foreach ($itemRows as &$row) {
|
||||||
$row['order_id'] = $order->id;
|
$row['order_id'] = $order->id;
|
||||||
@ -339,7 +263,7 @@ public function update(Order $order, array $data): Order
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Order $order): bool
|
public function destroy(Order $order): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($order) {
|
return DB::transaction(function () use ($order) {
|
||||||
$order->load('orderItems');
|
$order->load('orderItems');
|
||||||
|
|||||||
@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
class CategoryService
|
class CategoryService
|
||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return Category::select(['id', 'name'])->latest()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Category::query()
|
return Category::query()
|
||||||
@ -22,7 +17,12 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Category
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return Category::select(['id', 'name'])->latest()->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(array $data): Category
|
||||||
{
|
{
|
||||||
return Category::create($data);
|
return Category::create($data);
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ public function update(Category $category, array $data): Category
|
|||||||
return $category;
|
return $category;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Category $category): bool
|
public function destroy(Category $category): bool
|
||||||
{
|
{
|
||||||
return $category->delete();
|
return $category->delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
class CustomerService
|
class CustomerService
|
||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return Customer::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Customer::query()
|
return Customer::query()
|
||||||
@ -22,7 +17,12 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Customer
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return Customer::select(['id', 'name'])->orderBy('name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(array $data): Customer
|
||||||
{
|
{
|
||||||
return Customer::create($data);
|
return Customer::create($data);
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ public function update(Customer $customer, array $data): Customer
|
|||||||
return $customer;
|
return $customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Customer $customer): bool
|
public function destroy(Customer $customer): bool
|
||||||
{
|
{
|
||||||
return $customer->delete();
|
return $customer->delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,70 +2,48 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Master\Product;
|
namespace App\Services\Admin\Master\Product;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
use App\Services\StockMutationService;
|
use App\Services\StockMutationService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class ProductService
|
class ProductService
|
||||||
{
|
{
|
||||||
|
use HasRoleChecks;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ProductVariantService $variantService,
|
private ProductVariantService $variantService,
|
||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
private StockMutationService $stockMutationService,
|
private StockMutationService $stockMutationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getNames(): array
|
public function getNames(): Collection
|
||||||
{
|
{
|
||||||
return Product::where('status', '!=', 'deleted')
|
return Product::select(['id', 'name'])
|
||||||
->orderBy('name')
|
|
||||||
->pluck('name')
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
$products = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
|
||||||
->with([
|
|
||||||
'categories:id,name',
|
|
||||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
|
||||||
'productVariants.media',
|
|
||||||
])
|
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
|
||||||
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
|
||||||
->latest()
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$products->each(function ($product) {
|
|
||||||
$product->productVariants->each(function ($variant) {
|
|
||||||
$media = $variant->getMedia('photos');
|
|
||||||
$variant->photo_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return $products;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
$paginator = Product::query()
|
$paginator = Product::query()
|
||||||
->select(['id', 'name', 'slug', 'description', 'status'])
|
->select(['id', 'name', 'slug', 'description', 'status', 'rejection_reason'])
|
||||||
->with([
|
->with([
|
||||||
'categories:id,name',
|
'categories:id,name',
|
||||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
])
|
])
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
->when($filters['name'] ?? null, fn($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status))
|
||||||
->when($filters['category'] ?? null, fn ($q, $categoryId) => $q->whereHas('categories', function ($cq) use ($categoryId) {
|
->when($filters['category'] ?? null, fn($q, $categoryId) => $q->whereHas('categories', function ($cq) use ($categoryId) {
|
||||||
$cq->where('categories.id', $categoryId);
|
$cq->where('categories.id', $categoryId);
|
||||||
}))
|
}))
|
||||||
->when(($filters['stock'] ?? null) === 'empty', function ($q) {
|
->when(($filters['stock'] ?? null) === 'empty', function ($q) {
|
||||||
@ -80,20 +58,24 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
$paginator->getCollection()->each(function ($product) {
|
$paginator->getCollection()->each(function ($product) {
|
||||||
$product->productVariants->each(function ($variant) {
|
$product->productVariants->each(function ($variant) {
|
||||||
$media = $variant->getMedia('photos');
|
$media = $variant->getMedia('photos');
|
||||||
$variant->photo_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray();
|
$variant->photo_urls = $media->map(fn($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Product
|
public function store(array $data): Product
|
||||||
{
|
{
|
||||||
$product = DB::transaction(function () use ($data) {
|
$status = self::hasAnyRole([Role::DEVELOPER, Role::OWNER])
|
||||||
|
? ($data['status'] ?? ProductStatus::ACTIVE)
|
||||||
|
: ProductStatus::PENDING;
|
||||||
|
|
||||||
|
$product = DB::transaction(function () use ($data, $status) {
|
||||||
$product = Product::create([
|
$product = Product::create([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'description' => $data['description'] ?? null,
|
'description' => $data['description'] ?? null,
|
||||||
'status' => $data['status'] ?? 'active',
|
'status' => $status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$product->categories()->sync($data['category_ids']);
|
$product->categories()->sync($data['category_ids']);
|
||||||
@ -102,7 +84,7 @@ public function create(array $data): Product
|
|||||||
|
|
||||||
// Bulk insert variants
|
// Bulk insert variants
|
||||||
$now = now();
|
$now = now();
|
||||||
$variantRows = collect($data['variants'])->map(fn ($v) => [
|
$variantRows = collect($data['variants'])->map(fn($v) => [
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'name' => $v['name'],
|
'name' => $v['name'],
|
||||||
'stock' => $v['stock'],
|
'stock' => $v['stock'],
|
||||||
@ -116,7 +98,7 @@ public function create(array $data): Product
|
|||||||
|
|
||||||
// Map variant name -> variant ID
|
// Map variant name -> variant ID
|
||||||
$insertedVariants = ProductVariant::where('product_id', $product->id)->get();
|
$insertedVariants = ProductVariant::where('product_id', $product->id)->get();
|
||||||
$variantMap = $insertedVariants->mapWithKeys(fn ($v) => [$v->name => $v->id]);
|
$variantMap = $insertedVariants->mapWithKeys(fn($v) => [$v->name => $v->id]);
|
||||||
|
|
||||||
// Bulk insert prices
|
// Bulk insert prices
|
||||||
$priceRows = [];
|
$priceRows = [];
|
||||||
@ -160,9 +142,9 @@ public function create(array $data): Product
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Produk Baru',
|
title: 'Produk Baru',
|
||||||
body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
|
body: "Produk \"{$product->name}\" berhasil ditambahkan" . ' oleh ' . auth()->user()->full_name . '.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -180,7 +162,7 @@ public function getForEdit(Product $product): array
|
|||||||
$variants = $product->productVariants->map(function (ProductVariant $variant) {
|
$variants = $product->productVariants->map(function (ProductVariant $variant) {
|
||||||
$media = $variant->getMedia('photos');
|
$media = $variant->getMedia('photos');
|
||||||
$photoKeys = $media->pluck('file_name')->toArray();
|
$photoKeys = $media->pluck('file_name')->toArray();
|
||||||
$photoUrls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray();
|
$photoUrls = $media->map(fn($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
@ -190,7 +172,7 @@ public function getForEdit(Product $product): array
|
|||||||
'retail_stock' => $variant->retail_stock,
|
'retail_stock' => $variant->retail_stock,
|
||||||
'photo_keys' => $photoKeys,
|
'photo_keys' => $photoKeys,
|
||||||
'photo_urls' => $photoUrls,
|
'photo_urls' => $photoUrls,
|
||||||
'prices' => $variant->productPrices->map(fn ($p) => [
|
'prices' => $variant->productPrices->map(fn($p) => [
|
||||||
'type' => $p->type->value,
|
'type' => $p->type->value,
|
||||||
'price' => $p->price,
|
'price' => $p->price,
|
||||||
]),
|
]),
|
||||||
@ -209,11 +191,19 @@ public function getForEdit(Product $product): array
|
|||||||
|
|
||||||
public function update(Product $product, array $data): Product
|
public function update(Product $product, array $data): Product
|
||||||
{
|
{
|
||||||
|
$this->assertNotPending($product);
|
||||||
|
|
||||||
$product = DB::transaction(function () use ($product, $data) {
|
$product = DB::transaction(function () use ($product, $data) {
|
||||||
|
// Auto-resubmit: non-verifier editing rejected product → status becomes pending
|
||||||
|
$newStatus = $data['status'] ?? $product->status;
|
||||||
|
if ($product->status === ProductStatus::REJECTED && ! self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) {
|
||||||
|
$newStatus = ProductStatus::PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
$product->update([
|
$product->update([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'description' => $data['description'] ?? null,
|
'description' => $data['description'] ?? null,
|
||||||
'status' => $data['status'] ?? $product->status,
|
'status' => $newStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$product->categories()->sync($data['category_ids']);
|
$product->categories()->sync($data['category_ids']);
|
||||||
@ -240,7 +230,7 @@ public function update(Product $product, array $data): Product
|
|||||||
$existingVariantsMap = ProductVariant::whereIn('id', $existingVariantIds)
|
$existingVariantsMap = ProductVariant::whereIn('id', $existingVariantIds)
|
||||||
->with('media')
|
->with('media')
|
||||||
->get()
|
->get()
|
||||||
->mapWithKeys(fn ($v) => [$v->id => $v]);
|
->mapWithKeys(fn($v) => [$v->id => $v]);
|
||||||
|
|
||||||
// Collect old stock data + identify changed variants
|
// Collect old stock data + identify changed variants
|
||||||
$oldStockDataMap = [];
|
$oldStockDataMap = [];
|
||||||
@ -266,7 +256,7 @@ public function update(Product $product, array $data): Product
|
|||||||
// Bulk update changed variants (only changed ones, not all)
|
// Bulk update changed variants (only changed ones, not all)
|
||||||
if ($changedIds !== []) {
|
if ($changedIds !== []) {
|
||||||
$changedUpdates = collect($data['variants'])
|
$changedUpdates = collect($data['variants'])
|
||||||
->filter(fn ($v) => isset($v['id']) && in_array($v['id'], $changedIds));
|
->filter(fn($v) => isset($v['id']) && in_array($v['id'], $changedIds));
|
||||||
|
|
||||||
foreach ($changedUpdates as $variantData) {
|
foreach ($changedUpdates as $variantData) {
|
||||||
$existingVariantsMap[$variantData['id']]->update([
|
$existingVariantsMap[$variantData['id']]->update([
|
||||||
@ -279,11 +269,11 @@ public function update(Product $product, array $data): Product
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bulk create new variants
|
// Bulk create new variants
|
||||||
$newVariantsData = collect($data['variants'])->filter(fn ($v) => ! isset($v['id']));
|
$newVariantsData = collect($data['variants'])->filter(fn($v) => ! isset($v['id']));
|
||||||
$newVariantIdMap = [];
|
$newVariantIdMap = [];
|
||||||
|
|
||||||
if ($newVariantsData->isNotEmpty()) {
|
if ($newVariantsData->isNotEmpty()) {
|
||||||
$newVariantRows = $newVariantsData->map(fn ($v) => [
|
$newVariantRows = $newVariantsData->map(fn($v) => [
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'name' => $v['name'],
|
'name' => $v['name'],
|
||||||
'stock' => $v['stock'],
|
'stock' => $v['stock'],
|
||||||
@ -300,7 +290,7 @@ public function update(Product $product, array $data): Product
|
|||||||
->whereIn('name', $newVariantsData->pluck('name')->toArray())
|
->whereIn('name', $newVariantsData->pluck('name')->toArray())
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$newVariantIdMap = $newlyCreated->mapWithKeys(fn ($v) => [$v->name => $v->id])->toArray();
|
$newVariantIdMap = $newlyCreated->mapWithKeys(fn($v) => [$v->name => $v->id])->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build variant_id lookup: existing by id, new by name
|
// Build variant_id lookup: existing by id, new by name
|
||||||
@ -364,7 +354,7 @@ public function update(Product $product, array $data): Product
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$changedModels = collect($changedIds)->map(fn ($id) => $existingVariantsMap[$id])->filter();
|
$changedModels = collect($changedIds)->map(fn($id) => $existingVariantsMap[$id])->filter();
|
||||||
$this->stockMutationService->recordBulkAdjustment(
|
$this->stockMutationService->recordBulkAdjustment(
|
||||||
$changedModels,
|
$changedModels,
|
||||||
$oldStockDataMap,
|
$oldStockDataMap,
|
||||||
@ -403,17 +393,19 @@ public function update(Product $product, array $data): Product
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Produk Diperbarui',
|
title: 'Produk Diperbarui',
|
||||||
body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
body: "Produk \"{$product->name}\" berhasil diperbarui" . ' oleh ' . auth()->user()->full_name . '.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return $product;
|
return $product;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Product $product): bool
|
public function destroy(Product $product): bool
|
||||||
{
|
{
|
||||||
|
$this->assertNotPending($product);
|
||||||
|
|
||||||
$result = DB::transaction(function () use ($product) {
|
$result = DB::transaction(function () use ($product) {
|
||||||
$product->productVariants->each(function (ProductVariant $variant) {
|
$product->productVariants->each(function (ProductVariant $variant) {
|
||||||
$variant->productPrices()->delete();
|
$variant->productPrices()->delete();
|
||||||
@ -427,9 +419,9 @@ public function delete(Product $product): bool
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Produk Dihapus',
|
title: 'Produk Dihapus',
|
||||||
body: "Produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
body: "Produk \"{$product->name}\" berhasil dihapus" . ' oleh ' . auth()->user()->full_name . '.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -438,8 +430,65 @@ public function delete(Product $product): bool
|
|||||||
|
|
||||||
public function toggleStatus(Product $product): void
|
public function toggleStatus(Product $product): void
|
||||||
{
|
{
|
||||||
|
$this->assertNotPending($product);
|
||||||
|
|
||||||
$product->update([
|
$product->update([
|
||||||
'status' => $product->status->value === 'active' ? 'inactive' : 'active',
|
'status' => $product->status->value === 'active' ? 'inactive' : 'active',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function approve(Product $product): void
|
||||||
|
{
|
||||||
|
$product->update([
|
||||||
|
'status' => ProductStatus::ACTIVE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
NotificationService::notify(
|
||||||
|
roles: [Role::OWNER, Role::DEVELOPER],
|
||||||
|
title: 'Produk Disetujui',
|
||||||
|
body: "Produk \"{$product->name}\" telah disetujui oleh " . auth()->user()->full_name . '.',
|
||||||
|
url: route('admin.master.products.index'),
|
||||||
|
additionalUser: $product->createdBy ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reject(Product $product, string $reason = ''): void
|
||||||
|
{
|
||||||
|
$product->update([
|
||||||
|
'status' => ProductStatus::REJECTED,
|
||||||
|
'rejection_reason' => $reason,
|
||||||
|
]);
|
||||||
|
|
||||||
|
NotificationService::notify(
|
||||||
|
roles: [Role::OWNER, Role::DEVELOPER],
|
||||||
|
title: 'Produk Ditolak',
|
||||||
|
body: "Produk \"{$product->name}\" telah ditolak oleh " . auth()->user()->full_name . '.',
|
||||||
|
url: route('admin.master.products.index'),
|
||||||
|
additionalUser: $product->createdBy ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resubmit(Product $product): void
|
||||||
|
{
|
||||||
|
$product->update([
|
||||||
|
'status' => ProductStatus::PENDING,
|
||||||
|
'rejection_reason' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
NotificationService::notify(
|
||||||
|
roles: [Role::OWNER, Role::DEVELOPER],
|
||||||
|
title: 'Produk Diajukan Ulang',
|
||||||
|
body: "Produk \"{$product->name}\" telah diajukan ulang oleh " . auth()->user()->full_name . '.',
|
||||||
|
url: route('admin.master.products.index'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertNotPending(Product $product): void
|
||||||
|
{
|
||||||
|
if ($product->status === ProductStatus::PENDING && ! self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'status' => 'Produk sedang menunggu verifikasi dan tidak dapat diubah.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Master\Product;
|
namespace App\Services\Admin\Master\Product;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\ProductStatus;
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
@ -13,13 +17,66 @@
|
|||||||
|
|
||||||
class ProductVariantService
|
class ProductVariantService
|
||||||
{
|
{
|
||||||
use RegistersMedia;
|
use HasRoleChecks, RegistersMedia;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
private StockMutationService $stockMutationService,
|
private StockMutationService $stockMutationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function getForRestock(): array
|
||||||
|
{
|
||||||
|
return Product::query()
|
||||||
|
->select(['id', 'name', 'status'])
|
||||||
|
->with([
|
||||||
|
'productVariants:id,product_id,name,stock,reject_stock',
|
||||||
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
|
])
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->each(function (Product $product) {
|
||||||
|
$product->productVariants->each(function (ProductVariant $variant) {
|
||||||
|
$media = $variant->getFirstMedia('photos');
|
||||||
|
$variant->photo_url = $media
|
||||||
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$capitalPrice = $variant->productPrices
|
||||||
|
->first(fn ($price) => $price->type === PriceType::CAPITAL);
|
||||||
|
$variant->capital_price = $capitalPrice?->price ?? 0;
|
||||||
|
|
||||||
|
$rejectPrice = $variant->productPrices
|
||||||
|
->first(fn ($price) => $price->type === PriceType::REJECT);
|
||||||
|
$variant->reject_price = $rejectPrice?->price ?? 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getForTransaction(): array
|
||||||
|
{
|
||||||
|
return Product::query()
|
||||||
|
->select(['id', 'name', 'status'])
|
||||||
|
->with([
|
||||||
|
'productVariants:id,product_id,name,stock,reject_stock',
|
||||||
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
|
])
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->each(function (Product $product) {
|
||||||
|
$product->productVariants->each(function (ProductVariant $variant) {
|
||||||
|
$media = $variant->getFirstMedia('photos');
|
||||||
|
$variant->photo_url = $media
|
||||||
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$prices = $variant->productPrices->mapWithKeys(fn ($p) => [$p->type->value => $p->price]);
|
||||||
|
$variant->prices = $prices;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function getForEdit(ProductVariant $variant): array
|
public function getForEdit(ProductVariant $variant): array
|
||||||
{
|
{
|
||||||
$variant->load([
|
$variant->load([
|
||||||
@ -49,6 +106,8 @@ public function getForEdit(ProductVariant $variant): array
|
|||||||
|
|
||||||
public function update(ProductVariant $variant, array $data): ProductVariant
|
public function update(ProductVariant $variant, array $data): ProductVariant
|
||||||
{
|
{
|
||||||
|
$this->assertNotPending($variant->product);
|
||||||
|
|
||||||
DB::transaction(function () use ($variant, $data) {
|
DB::transaction(function () use ($variant, $data) {
|
||||||
$oldData = $variant->only(['stock', 'reject_stock', 'retail_stock']);
|
$oldData = $variant->only(['stock', 'reject_stock', 'retail_stock']);
|
||||||
|
|
||||||
@ -83,7 +142,7 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Varian Diperbarui',
|
title: 'Varian Diperbarui',
|
||||||
body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
@ -92,8 +151,10 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
|||||||
return $variant->fresh();
|
return $variant->fresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Product $product, ProductVariant $variant): bool
|
public function destroy(Product $product, ProductVariant $variant): bool
|
||||||
{
|
{
|
||||||
|
$this->assertNotPending($product);
|
||||||
|
|
||||||
$result = DB::transaction(function () use ($variant) {
|
$result = DB::transaction(function () use ($variant) {
|
||||||
$variant->productPrices()->delete();
|
$variant->productPrices()->delete();
|
||||||
$variant->clearMediaCollection('photos');
|
$variant->clearMediaCollection('photos');
|
||||||
@ -102,7 +163,7 @@ public function delete(Product $product, ProductVariant $variant): bool
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Varian Dihapus',
|
title: 'Varian Dihapus',
|
||||||
body: "Varian \"{$variant->name}\" dari produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
body: "Varian \"{$variant->name}\" dari produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
@ -134,6 +195,8 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
|||||||
{
|
{
|
||||||
$quantity = (int) $data['quantity'];
|
$quantity = (int) $data['quantity'];
|
||||||
|
|
||||||
|
$this->assertNotPending($variant->product);
|
||||||
|
|
||||||
if ($variant->stock < $quantity) {
|
if ($variant->stock < $quantity) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'quantity' => "Stok bagus tidak mencukupi. Stok tersedia: {$variant->stock}.",
|
'quantity' => "Stok bagus tidak mencukupi. Stok tersedia: {$variant->stock}.",
|
||||||
@ -161,7 +224,7 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Transfer Stok',
|
title: 'Transfer Stok',
|
||||||
body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.',
|
body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.master.products.index'),
|
url: route('admin.master.products.index'),
|
||||||
@ -169,4 +232,13 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
|||||||
|
|
||||||
return $variant->fresh();
|
return $variant->fresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function assertNotPending(Product $product): void
|
||||||
|
{
|
||||||
|
if ($product->status === ProductStatus::PENDING && ! self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'status' => 'Produk sedang menunggu verifikasi dan tidak dapat diubah.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
use App\Services\S3PresignedService;
|
use App\Services\S3PresignedService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class RawMaterialService
|
class RawMaterialService
|
||||||
@ -18,27 +17,6 @@ public function __construct(
|
|||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return RawMaterial::select(['id', 'name', 'unit', 'is_active'])
|
|
||||||
->with([
|
|
||||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
|
||||||
'rawMaterialPrices.media',
|
|
||||||
])
|
|
||||||
->when($filters['is_active'] ?? null, fn ($q, $isActive) => $q->where('is_active', $isActive === 'true'))
|
|
||||||
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
|
||||||
->latest()
|
|
||||||
->get()
|
|
||||||
->each(function ($rawMaterial) {
|
|
||||||
$rawMaterial->rawMaterialPrices->each(function ($price) {
|
|
||||||
$media = $price->getMedia('photos');
|
|
||||||
$price->photo_url = $media->first()
|
|
||||||
? $this->s3Service->getTemporaryUrl($media->first()->file_name)
|
|
||||||
: null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
$paginator = RawMaterial::query()
|
$paginator = RawMaterial::query()
|
||||||
@ -71,7 +49,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): RawMaterial
|
public function store(array $data): RawMaterial
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$rawMaterial = RawMaterial::create([
|
$rawMaterial = RawMaterial::create([
|
||||||
@ -220,7 +198,7 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(RawMaterial $rawMaterial): bool
|
public function destroy(RawMaterial $rawMaterial): bool
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($rawMaterial) {
|
return DB::transaction(function () use ($rawMaterial) {
|
||||||
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
|
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Master\RawMaterial;
|
namespace App\Services\Admin\Master\RawMaterial;
|
||||||
|
|
||||||
|
use App\Enums\Role;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
use App\Models\RawMaterialPrice;
|
use App\Models\RawMaterialPrice;
|
||||||
use App\Services\Concerns\RegistersMedia;
|
use App\Services\Concerns\RegistersMedia;
|
||||||
@ -17,6 +18,27 @@ public function __construct(
|
|||||||
private S3PresignedService $s3Service,
|
private S3PresignedService $s3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function getForCutting(): array
|
||||||
|
{
|
||||||
|
return RawMaterial::query()
|
||||||
|
->select(['id', 'name', 'unit', 'is_active'])
|
||||||
|
->with([
|
||||||
|
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||||
|
])
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->each(function (RawMaterial $rawMaterial) {
|
||||||
|
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
|
||||||
|
$media = $price->getFirstMedia('photos');
|
||||||
|
$price->photo_url = $media
|
||||||
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public function getForEdit(RawMaterialPrice $variant): array
|
public function getForEdit(RawMaterialPrice $variant): array
|
||||||
{
|
{
|
||||||
$variant->load('media');
|
$variant->load('media');
|
||||||
@ -61,7 +83,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Varian Diperbarui',
|
title: 'Varian Diperbarui',
|
||||||
body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.master.raw-materials.index'),
|
url: route('admin.master.raw-materials.index'),
|
||||||
@ -70,7 +92,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
|
|||||||
return $variant->fresh();
|
return $variant->fresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(RawMaterial $rawMaterial, RawMaterialPrice $variant): bool
|
public function destroy(RawMaterial $rawMaterial, RawMaterialPrice $variant): bool
|
||||||
{
|
{
|
||||||
$result = DB::transaction(function () use ($variant) {
|
$result = DB::transaction(function () use ($variant) {
|
||||||
$variant->clearMediaCollection('photos');
|
$variant->clearMediaCollection('photos');
|
||||||
@ -79,7 +101,7 @@ public function delete(RawMaterial $rawMaterial, RawMaterialPrice $variant): boo
|
|||||||
});
|
});
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||||
title: 'Varian Dihapus',
|
title: 'Varian Dihapus',
|
||||||
body: "Varian \"{$variant->variant}\" dari bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
body: "Varian \"{$variant->variant}\" dari bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.master.raw-materials.index'),
|
url: route('admin.master.raw-materials.index'),
|
||||||
|
|||||||
@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
class SupplierService
|
class SupplierService
|
||||||
{
|
{
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return Supplier::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Supplier::query()
|
return Supplier::query()
|
||||||
@ -22,7 +17,12 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data): Supplier
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return Supplier::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(array $data): Supplier
|
||||||
{
|
{
|
||||||
return Supplier::create($data);
|
return Supplier::create($data);
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ public function update(Supplier $supplier, array $data): Supplier
|
|||||||
return $supplier;
|
return $supplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Supplier $supplier): bool
|
public function destroy(Supplier $supplier): bool
|
||||||
{
|
{
|
||||||
return $supplier->delete();
|
return $supplier->delete();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,37 +2,32 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin\Settings;
|
namespace App\Services\Admin\Settings;
|
||||||
|
|
||||||
|
use App\Concerns\HasRoleChecks;
|
||||||
|
use App\Enums\Role;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role as SpatieRole;
|
||||||
|
|
||||||
class RoleService
|
class RoleService
|
||||||
{
|
{
|
||||||
public function getAll(): Collection
|
use HasRoleChecks;
|
||||||
{
|
|
||||||
return Role::withCount('permissions')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Role::query()
|
return SpatieRole::query()
|
||||||
|
->select(['id', 'name'])
|
||||||
->withCount('permissions')
|
->withCount('permissions')
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getById(int $id): Role
|
public function store(array $data): Role
|
||||||
{
|
|
||||||
return Role::with('permissions')->findOrFail($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(array $data): Role
|
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($data) {
|
return DB::transaction(function () use ($data) {
|
||||||
$role = Role::create(['name' => $data['name']]);
|
$role = SpatieRole::create(['name' => $data['name']]);
|
||||||
$role->syncPermissions($data['permissions']);
|
$role->syncPermissions($data['permissions']);
|
||||||
|
|
||||||
return $role;
|
return $role;
|
||||||
@ -49,7 +44,7 @@ public function update(Role $role, array $data): Role
|
|||||||
return $role->fresh('permissions');
|
return $role->fresh('permissions');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Role $role): bool
|
public function destroy(Role $role): bool
|
||||||
{
|
{
|
||||||
return $role->delete();
|
return $role->delete();
|
||||||
}
|
}
|
||||||
@ -61,4 +56,22 @@ public function getPermissionsByModule(): array
|
|||||||
->map(fn ($group) => $group->pluck('name')->map(fn ($name) => explode('.', $name, 2)[1])->values()->toArray())
|
->map(fn ($group) => $group->pluck('name')->map(fn ($name) => explode('.', $name, 2)[1])->values()->toArray())
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getForEmployee(): Collection
|
||||||
|
{
|
||||||
|
$query = SpatieRole::where('name', '!=', Role::DEVELOPER->value);
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (self::hasAnyRole([Role::ADMIN_TOKO, Role::DIREKTUR])) {
|
||||||
|
$query->where('name', '!=', Role::ADMIN_BAHAN_BAKU->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO])) {
|
||||||
|
$userRoles = $user->roles->pluck('name');
|
||||||
|
$query->whereIn('name', $userRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->get(['id', 'name']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
578
app/Services/AnalysisService.php
Normal file
578
app/Services/AnalysisService.php
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
|
use App\Enums\OrderChannel;
|
||||||
|
use App\Enums\OrderStatus;
|
||||||
|
use App\Enums\PaymentType;
|
||||||
|
use App\Enums\PriceType;
|
||||||
|
use App\Enums\RawMaterialUnit;
|
||||||
|
use App\Models\Attendance;
|
||||||
|
use App\Models\CashAccount;
|
||||||
|
use App\Models\Employee;
|
||||||
|
use App\Models\EmployeeAdvance;
|
||||||
|
use App\Models\Expense;
|
||||||
|
use App\Models\LeaveRequest;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\ProductVariant;
|
||||||
|
use App\Models\Purchase;
|
||||||
|
use App\Models\PurchaseItem;
|
||||||
|
use App\Models\RawMaterialPrice;
|
||||||
|
use App\Models\RestockItem;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AnalysisService
|
||||||
|
{
|
||||||
|
public function getAttendanceStats(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth();
|
||||||
|
$end = $endDate ? Carbon::parse($endDate) : Carbon::now();
|
||||||
|
|
||||||
|
$employees = Employee::whereHas('user', fn ($q) => $q
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereHas('roles', fn ($r) => $r
|
||||||
|
->whereHas('permissions', fn ($p) => $p
|
||||||
|
->where('name', 'attendances.create')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->where('join_date', '<=', $end)
|
||||||
|
->where(function ($q) use ($start) {
|
||||||
|
$q->whereNull('resign_date')->orWhere('resign_date', '>=', $start);
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$employeeIds = $employees->pluck('id');
|
||||||
|
$employeeCount = $employeeIds->count();
|
||||||
|
|
||||||
|
$present = Attendance::whereIn('employee_id', $employeeIds)
|
||||||
|
->whereBetween('attendance_date', [$start->toDateString(), $end->toDateString()])
|
||||||
|
->distinct('employee_id')
|
||||||
|
->count('employee_id');
|
||||||
|
|
||||||
|
$onLeave = LeaveRequest::approved()
|
||||||
|
->where('start_date', '<=', $end)
|
||||||
|
->where('end_date', '>=', $start)
|
||||||
|
->whereIn('employee_id', $employeeIds)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$absent = max(0, $employeeCount - $present - $onLeave);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_employees' => $employeeCount,
|
||||||
|
'present' => $present,
|
||||||
|
'absent' => $absent,
|
||||||
|
'on_leave' => $onLeave,
|
||||||
|
'percentage' => $employeeCount > 0 ? round(($present / $employeeCount) * 100) : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMyAttendance(User $user, ?string $startDate, ?string $endDate): ?array
|
||||||
|
{
|
||||||
|
$employee = $user->employee;
|
||||||
|
|
||||||
|
if (! $employee) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = $startDate ? Carbon::parse($startDate) : Carbon::now()->startOfMonth();
|
||||||
|
$end = $endDate ? Carbon::parse($endDate) : Carbon::now();
|
||||||
|
|
||||||
|
$workingDays = 0;
|
||||||
|
$current = $start->copy();
|
||||||
|
|
||||||
|
while ($current->lte($end)) {
|
||||||
|
if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) {
|
||||||
|
$workingDays++;
|
||||||
|
}
|
||||||
|
$current->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
$present = Attendance::where('employee_id', $employee->id)
|
||||||
|
->whereBetween('attendance_date', [$start, $end])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$leaveDays = LeaveRequest::approved()
|
||||||
|
->where('employee_id', $employee->id)
|
||||||
|
->where('start_date', '<=', $end)
|
||||||
|
->where('end_date', '>=', $start)
|
||||||
|
->get()
|
||||||
|
->reduce(function ($carry, $leave) use ($start, $end) {
|
||||||
|
$leaveStart = max($leave->start_date->timestamp, $start->timestamp);
|
||||||
|
$leaveEnd = min($leave->end_date->timestamp, $end->timestamp);
|
||||||
|
$days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1;
|
||||||
|
|
||||||
|
return $carry + max(0, $days);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
$absent = max(0, $workingDays - $present - $leaveDays);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_days' => $workingDays,
|
||||||
|
'present_days' => $present,
|
||||||
|
'absent_days' => $absent,
|
||||||
|
'leave_days' => $leaveDays,
|
||||||
|
'percentage' => $workingDays > 0 ? round(($present / $workingDays) * 100) : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCashOverview(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$cashAccount = CashAccount::first();
|
||||||
|
|
||||||
|
if (! $cashAccount) {
|
||||||
|
return [
|
||||||
|
'total_balance' => 0,
|
||||||
|
'total_transactions' => 0,
|
||||||
|
'total_deposit' => 0,
|
||||||
|
'total_withdrawal' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$transactions = $cashAccount->cashTransactions();
|
||||||
|
$this->applyDateFilter($transactions, $startDate, $endDate, 'cash_transactions.created_at');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_balance' => $cashAccount->balance,
|
||||||
|
'total_transactions' => (clone $transactions)->count(),
|
||||||
|
'total_deposit' => (int) (clone $transactions)->where('type', CashTransactionType::DEPOSIT)->sum('amount'),
|
||||||
|
'total_withdrawal' => (int) (clone $transactions)->where('type', CashTransactionType::WITHDRAWAL)->sum('amount'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRawMaterialStock(): array
|
||||||
|
{
|
||||||
|
$query = PurchaseItem::query()
|
||||||
|
->join('purchases', 'purchase_items.purchase_id', '=', 'purchases.id')
|
||||||
|
->leftJoin('raw_material_prices', 'purchase_items.raw_material_price_id', '=', 'raw_material_prices.id')
|
||||||
|
->leftJoin('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id');
|
||||||
|
|
||||||
|
$items = $query->select('purchase_items.*', 'raw_materials.unit')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$totalQty = $items->sum('quantity');
|
||||||
|
$totalValue = $items->sum('subtotal');
|
||||||
|
|
||||||
|
$byUnit = [
|
||||||
|
'yard' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::YARD)->sum('quantity'),
|
||||||
|
'meter' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::METER)->sum('quantity'),
|
||||||
|
'kilogram' => $items->filter(fn ($i) => $i->unit === RawMaterialUnit::KG)->sum('quantity'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_stock' => $totalQty,
|
||||||
|
'total_value' => $totalValue,
|
||||||
|
'by_unit' => $byUnit,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductStock(): array
|
||||||
|
{
|
||||||
|
$query = RestockItem::query()
|
||||||
|
->join('restocks', 'restock_items.restock_id', '=', 'restocks.id');
|
||||||
|
|
||||||
|
$items = $query->select('restock_items.*', 'restocks.stock_type')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$totalQty = $items->sum('quantity');
|
||||||
|
$totalValue = $items->sum('subtotal');
|
||||||
|
|
||||||
|
$byType = $items->groupBy(fn ($i) => $i->stock_type ?? 'unknown')
|
||||||
|
->map(fn ($group) => $group->sum('quantity'))
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_stock' => $totalQty,
|
||||||
|
'total_value' => $totalValue,
|
||||||
|
'by_type' => $byType,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRevenueSummary(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$stats = (clone $query)
|
||||||
|
->selectRaw('COUNT(*) as total_orders')
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||||
|
->selectRaw('COALESCE(SUM(discount), 0) as total_discount')
|
||||||
|
->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction')
|
||||||
|
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_revenue' => (int) $stats->total_revenue,
|
||||||
|
'total_discount' => (int) $stats->total_discount,
|
||||||
|
'total_deduction' => (int) $stats->total_deduction,
|
||||||
|
'cogs' => (int) $stats->total_cogs,
|
||||||
|
'net' => (int) $stats->total_revenue - (int) $stats->total_cogs,
|
||||||
|
'total_orders' => (int) $stats->total_orders,
|
||||||
|
'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonthlyRevenue(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$monthly = (clone $query)
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount) - SUM(cogs), 0) as net')
|
||||||
|
->selectRaw('COALESCE(SUM(discount), 0) as discount')
|
||||||
|
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
|
||||||
|
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
|
||||||
|
->get()
|
||||||
|
->map(fn ($item) => [
|
||||||
|
'month' => $item->month,
|
||||||
|
'total' => (int) $item->total,
|
||||||
|
'net' => (int) $item->net,
|
||||||
|
'discount' => (int) $item->discount,
|
||||||
|
'deduction' => (int) $item->deduction,
|
||||||
|
'cogs' => (int) $item->cogs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $monthly->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$monthly = (clone $query)
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'store' THEN total_amount ELSE 0 END), 0) as store")
|
||||||
|
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'shopee' THEN total_amount ELSE 0 END), 0) as shopee")
|
||||||
|
->selectRaw("COALESCE(SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END), 0) as tiktok")
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
|
||||||
|
->get()
|
||||||
|
->map(fn ($item) => [
|
||||||
|
'month' => $item->month,
|
||||||
|
'store' => (int) $item->store,
|
||||||
|
'shopee' => (int) $item->shopee,
|
||||||
|
'tiktok' => (int) $item->tiktok,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $monthly->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$data = (clone $query)
|
||||||
|
->select('payment_type')
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||||
|
->groupBy('payment_type')
|
||||||
|
->get()
|
||||||
|
->map(fn ($item) => [
|
||||||
|
'payment_type' => $item->payment_type,
|
||||||
|
'label' => $item->payment_type->label(),
|
||||||
|
'total' => (int) $item->total,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $data->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpenseSummary(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$expenseQuery = Expense::query();
|
||||||
|
$this->applyDateFilter($expenseQuery, $startDate, $endDate, 'expenses.created_at');
|
||||||
|
|
||||||
|
$advanceQuery = EmployeeAdvance::where('status', 'paid');
|
||||||
|
$this->applyDateFilter($advanceQuery, $startDate, $endDate, 'employee_advances.created_at');
|
||||||
|
|
||||||
|
$purchaseQuery = Purchase::query();
|
||||||
|
$this->applyDateFilter($purchaseQuery, $startDate, $endDate, 'purchases.created_at');
|
||||||
|
|
||||||
|
$expenseTotal = (clone $expenseQuery)->sum('amount');
|
||||||
|
$advanceTotal = (clone $advanceQuery)->sum('amount');
|
||||||
|
$purchaseTotal = (clone $purchaseQuery)->sum('total');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => (int) ($expenseTotal + $advanceTotal + $purchaseTotal),
|
||||||
|
'purchase_total' => (int) $purchaseTotal,
|
||||||
|
'expense_total' => (int) $expenseTotal,
|
||||||
|
'advance_total' => (int) $advanceTotal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$expenseMonthly = Expense::query();
|
||||||
|
$this->applyDateFilter($expenseMonthly, $startDate, $endDate, 'expenses.created_at');
|
||||||
|
|
||||||
|
$expenseByMonth = (clone $expenseMonthly)->toBase()
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw('COALESCE(SUM(amount), 0) as expense')
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->get()
|
||||||
|
->keyBy('month');
|
||||||
|
|
||||||
|
$purchaseMonthly = Purchase::query();
|
||||||
|
$this->applyDateFilter($purchaseMonthly, $startDate, $endDate, 'purchases.created_at');
|
||||||
|
|
||||||
|
$purchaseByMonth = (clone $purchaseMonthly)->toBase()
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw('COALESCE(SUM(total), 0) as purchase')
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->get()
|
||||||
|
->keyBy('month');
|
||||||
|
|
||||||
|
$advanceMonthly = EmployeeAdvance::where('status', 'paid');
|
||||||
|
$this->applyDateFilter($advanceMonthly, $startDate, $endDate, 'employee_advances.created_at');
|
||||||
|
|
||||||
|
$advanceByMonth = (clone $advanceMonthly)->toBase()
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw('COALESCE(SUM(amount), 0) as advance')
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->get()
|
||||||
|
->keyBy('month');
|
||||||
|
|
||||||
|
$allMonths = [];
|
||||||
|
foreach ([$expenseByMonth, $purchaseByMonth, $advanceByMonth] as $data) {
|
||||||
|
foreach ($data as $month => $row) {
|
||||||
|
if (! array_key_exists($month, $allMonths)) {
|
||||||
|
$allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allMonths as $month => &$row) {
|
||||||
|
$row['purchase'] = (int) ($purchaseByMonth[$month]->purchase ?? 0);
|
||||||
|
$row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0);
|
||||||
|
$row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0);
|
||||||
|
$row['total'] = $row['purchase'] + $row['expense'] + $row['advance'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($allMonths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBusyHours(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$hours = range(0, 23);
|
||||||
|
$hourCounts = (clone $query)
|
||||||
|
->selectRaw('HOUR(created_at) as hour')
|
||||||
|
->selectRaw('COUNT(*) as orders')
|
||||||
|
->groupBy(DB::raw('HOUR(created_at)'))
|
||||||
|
->pluck('orders', 'hour')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return array_map(function ($h) use ($hourCounts) {
|
||||||
|
return [
|
||||||
|
'hour' => sprintf('%02d:00', $h),
|
||||||
|
'orders' => (int) ($hourCounts[$h] ?? 0),
|
||||||
|
];
|
||||||
|
}, $hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProfitMetrics(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$stats = (clone $query)
|
||||||
|
->selectRaw('COUNT(*) as total_orders')
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||||
|
->selectRaw('COALESCE(SUM(cogs), 0) as hpp')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$totalProductsSold = (clone $query)
|
||||||
|
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||||
|
->sum('order_items.quantity');
|
||||||
|
|
||||||
|
$grossProfit = $stats->total_revenue - $stats->hpp;
|
||||||
|
$netProfit = $grossProfit;
|
||||||
|
$profitMargin = $stats->total_revenue > 0 ? round(($netProfit / $stats->total_revenue) * 100, 1) : 0;
|
||||||
|
$aov = $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0;
|
||||||
|
$itemsPerTransaction = $stats->total_orders > 0 ? round($totalProductsSold / $stats->total_orders, 1) : 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_orders' => (int) $stats->total_orders,
|
||||||
|
'total_products_sold' => (int) $totalProductsSold,
|
||||||
|
'hpp' => (int) $stats->hpp,
|
||||||
|
'gross_profit' => (int) $grossProfit,
|
||||||
|
'net_profit' => (int) $netProfit,
|
||||||
|
'profit_margin' => $profitMargin,
|
||||||
|
'aov' => $aov,
|
||||||
|
'items_per_transaction' => $itemsPerTransaction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTopSuppliers(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Purchase::query();
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'purchases.created_at');
|
||||||
|
|
||||||
|
return (clone $query)->toBase()
|
||||||
|
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
|
||||||
|
->select('suppliers.name')
|
||||||
|
->selectRaw('COALESCE(SUM(purchases.total), 0) as total_amount')
|
||||||
|
->selectRaw('COUNT(*) as purchase_count')
|
||||||
|
->groupBy('suppliers.name')
|
||||||
|
->orderByDesc('total_amount')
|
||||||
|
->limit(5)
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTopCustomers(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id');
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
return (clone $query)->toBase()
|
||||||
|
->join('customers', 'orders.customer_id', '=', 'customers.id')
|
||||||
|
->select('customers.name')
|
||||||
|
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_amount')
|
||||||
|
->selectRaw('COUNT(*) as order_count')
|
||||||
|
->groupBy('customers.name')
|
||||||
|
->orderByDesc('total_amount')
|
||||||
|
->limit(5)
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTopProducts(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
return (clone $query)->toBase()
|
||||||
|
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||||
|
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
|
||||||
|
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||||
|
->select('products.name')
|
||||||
|
->selectRaw('SUM(order_items.quantity) as total_qty')
|
||||||
|
->selectRaw('COALESCE(SUM(order_items.subtotal), 0) as total_revenue')
|
||||||
|
->groupBy('products.name')
|
||||||
|
->orderByDesc('total_qty')
|
||||||
|
->limit(5)
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMarketingSales(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED)
|
||||||
|
->whereNotNull('orders.marketing_id');
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$orders = (clone $query)
|
||||||
|
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||||
|
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||||
|
->select('orders.marketing_id', 'user_profiles.full_name as marketing_name')
|
||||||
|
->selectRaw('COUNT(DISTINCT orders.id) as total_orders')
|
||||||
|
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_revenue')
|
||||||
|
->selectRaw('COALESCE(SUM(orders.subtotal), 0) as total_subtotal')
|
||||||
|
->selectRaw('COALESCE(SUM(orders.discount), 0) as total_discount')
|
||||||
|
->groupBy('orders.marketing_id', 'user_profiles.full_name')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$productCounts = (clone $query)
|
||||||
|
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||||
|
->selectRaw('orders.marketing_id, SUM(order_items.quantity) as total_qty')
|
||||||
|
->groupBy('orders.marketing_id')
|
||||||
|
->pluck('total_qty', 'marketing_id');
|
||||||
|
|
||||||
|
return $orders->map(function ($item) use ($productCounts) {
|
||||||
|
$totalOrders = (int) $item->total_orders;
|
||||||
|
$totalRevenue = (int) $item->total_revenue;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'marketing_name' => $item->marketing_name,
|
||||||
|
'total_orders' => $totalOrders,
|
||||||
|
'total_products_sold' => (int) ($productCounts[$item->marketing_id] ?? 0),
|
||||||
|
'total_revenue' => $totalRevenue,
|
||||||
|
'total_subtotal' => (int) $item->total_subtotal,
|
||||||
|
'total_discount' => (int) $item->total_discount,
|
||||||
|
'avg_order' => $totalOrders > 0 ? (int) ($totalRevenue / $totalOrders) : 0,
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrderStats(?string $startDate, ?string $endDate): array
|
||||||
|
{
|
||||||
|
$baseQuery = Order::query();
|
||||||
|
$this->applyDateFilter($baseQuery, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
$byChannel = collect(OrderChannel::values())->map(function ($channel) use ($baseQuery) {
|
||||||
|
$count = (clone $baseQuery)->where('channel', $channel)->count();
|
||||||
|
$label = OrderChannel::from($channel)->label();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'channel' => $channel,
|
||||||
|
'label' => $label,
|
||||||
|
'count' => $count,
|
||||||
|
'total' => (int) (clone $baseQuery)->where('channel', $channel)->sum('total_amount'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$byPaymentType = collect(PaymentType::values())->map(function ($paymentType) use ($baseQuery) {
|
||||||
|
$count = (clone $baseQuery)->where('payment_type', $paymentType)->count();
|
||||||
|
$label = PaymentType::from($paymentType)->label();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'payment_type' => $paymentType,
|
||||||
|
'label' => $label,
|
||||||
|
'count' => $count,
|
||||||
|
'total' => (int) (clone $baseQuery)->where('payment_type', $paymentType)->sum('total_amount'),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$byMarketing = (clone $baseQuery)
|
||||||
|
->whereNotNull('marketing_id')
|
||||||
|
->select('marketing_id')
|
||||||
|
->selectRaw('COUNT(*) as count')
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||||
|
->groupBy('marketing_id')
|
||||||
|
->with('marketing:id')
|
||||||
|
->get()
|
||||||
|
->map(fn($item) => [
|
||||||
|
'name' => $item->marketing?->userProfile->full_name ?? '-',
|
||||||
|
'count' => $item->count,
|
||||||
|
'total' => (int) $item->total,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$byStatus = collect(OrderStatus::values())->map(function ($status) use ($baseQuery) {
|
||||||
|
$count = (clone $baseQuery)->where('status', $status)->count();
|
||||||
|
$label = OrderStatus::from($status)->label();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => $status,
|
||||||
|
'label' => $label,
|
||||||
|
'count' => $count,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'by_channel' => $byChannel,
|
||||||
|
'by_payment_type' => $byPaymentType,
|
||||||
|
'by_marketing' => $byMarketing,
|
||||||
|
'by_status' => $byStatus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyDateFilter($query, ?string $startDate, ?string $endDate, string $dateColumn = 'created_at'): void
|
||||||
|
{
|
||||||
|
if ($startDate) {
|
||||||
|
$query->whereDate($dateColumn, '>=', $startDate);
|
||||||
|
}
|
||||||
|
if ($endDate) {
|
||||||
|
$query->whereDate($dateColumn, '<=', $endDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user