Refactor services to improve role checks and streamline data retrieval
- Updated CustomerService to simplify getAll method. - Refactored ProductService to utilize HasRoleChecks trait and improved role verification logic. - Enhanced ProductVariantService with new methods for fetching data for restocking and transactions. - Cleaned up RawMaterialService by removing unused methods and improving data retrieval. - Adjusted SupplierService to streamline getAll method. - Refactored RoleService to use Spatie's Role model and improved role filtering logic. - Updated NotificationService to handle role labels more effectively. - Improved StockMutationService by removing redundant paginated method. - Cleaned up various frontend components to directly accept necessary props instead of nested data objects. - Updated tests to reflect changes in service method names and ensure proper notification handling.
This commit is contained in:
parent
46af27e537
commit
0023309a8f
@ -66,10 +66,10 @@ ## Module Overview
|
||||
| 8 | Transaksi | `admin/manage/transactions` | `Admin/Manage/TransactionController` | `Admin/Manage/TransactionService` |
|
||||
| 9 | Restock | `admin/manage/restocks` | `Admin/Manage/RestockController` | `Admin/Manage/RestockService` |
|
||||
| 10 | Stok Opname | `admin/manage/stok-opnames` | (via StockMutationController) | — |
|
||||
| 11 | Kas Toko | `admin/finance/cash-accounts` | `Admin/Finance/CashAccountController` | `Admin/Finance/CashAccountService` |
|
||||
| 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/PayrollPeriodService` |
|
||||
| 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` |
|
||||
|
||||
@ -178,6 +178,9 @@ ### Urutan Isi Model
|
||||
public function categories(): BelongsToMany { ... }
|
||||
public function productVariants(): HasMany { ... }
|
||||
}
|
||||
|
||||
// ✅ Trait stacking — gabungkan dengan koma
|
||||
use HasFactory, SoftDeletes; // 1 baris, pisah koma
|
||||
```
|
||||
|
||||
### Casting — Wajib untuk Semua Tipe
|
||||
@ -698,7 +701,7 @@ ### Penamaan Method — HARUS KONSISTEN
|
||||
}
|
||||
|
||||
// 2. getAll() — untuk semua data (tanpa paginasi)
|
||||
public function getAll(array $filters = []): Collection
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Product::query()
|
||||
->select(['id', 'name'])
|
||||
@ -753,6 +756,36 @@ ### Property Declaration
|
||||
}
|
||||
```
|
||||
|
||||
### Urutan Method — KONSISTEN
|
||||
```php
|
||||
// ✅ Urutan method di service WAJIB konsisten:
|
||||
// 1. CRUD (paginated, getAll, store, update, destroy)
|
||||
// 2. Custom methods (business logic)
|
||||
// 3. Private helpers (di paling bawah)
|
||||
|
||||
class ProductService
|
||||
{
|
||||
// 1. CRUD
|
||||
public function paginated(...): LengthAwarePaginator { ... }
|
||||
public function getAll(...): Collection { ... }
|
||||
public function store(array $data): Product { ... }
|
||||
public function update(Product $product, array $data): Product { ... }
|
||||
public function destroy(Product $product): bool { ... }
|
||||
|
||||
// 2. Custom methods
|
||||
public function getNames(): array { ... }
|
||||
public function getForEdit(Product $product): array { ... }
|
||||
public function toggleStatus(Product $product): void { ... }
|
||||
public function approve(Product $product): void { ... }
|
||||
public function reject(Product $product, string $reason): void { ... }
|
||||
public function resubmit(Product $product): void { ... }
|
||||
|
||||
// 3. Private helpers (paling bawah)
|
||||
private function canVerify(): bool { ... }
|
||||
private function assertNotPending(Product $product): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Service Concerns (Traits)
|
||||
```php
|
||||
// ✅ Gunakan traits untuk kode yang berulang di banyak service
|
||||
@ -800,6 +833,64 @@ ### Service Concerns (Traits)
|
||||
return $restock;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. HasRoleChecks — untuk role checking
|
||||
use App\Concerns\HasRoleChecks;
|
||||
|
||||
class ProductService
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
public function store(array $data): Product
|
||||
{
|
||||
$status = self::hasAnyRole([Role::DEVELOPER, Role::OWNER])
|
||||
? ProductStatus::ACTIVE
|
||||
: ProductStatus::PENDING;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Trait Stacking — Gunakan Koma
|
||||
```php
|
||||
// ✅ Gabungkan multiple traits dengan koma dalam 1 baris
|
||||
class PayrollPeriodService
|
||||
{
|
||||
use HandlesCashTransactions, HasRoleChecks;
|
||||
}
|
||||
|
||||
class AttendanceService
|
||||
{
|
||||
use RegistersMedia, HasRoleChecks;
|
||||
}
|
||||
|
||||
// ❌ JANGAN pisah per baris
|
||||
class PayrollPeriodService
|
||||
{
|
||||
use HandlesCashTransactions;
|
||||
use HasRoleChecks; // JANGAN
|
||||
}
|
||||
```
|
||||
|
||||
### Role Checking — Gunakan RoleEnum
|
||||
```php
|
||||
// ✅ SELALU gunakan Role enum, JANGAN string
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
|
||||
class ProductService
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
public function store(array $data): Product
|
||||
{
|
||||
// ✅ BENAR — pakai Role enum
|
||||
if (self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) { ... }
|
||||
|
||||
// ❌ SALAH — pakai string
|
||||
if (auth()->user()->hasAnyRole(['developer', 'owner'])) { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cash Transaction Pattern
|
||||
@ -820,6 +911,44 @@ ### Stock Adjustment Pattern
|
||||
$this->adjustStock($model, $field, $quantity, $sign);
|
||||
```
|
||||
|
||||
### NotificationService — Gunakan RoleEnum
|
||||
```php
|
||||
// ✅ SELALU gunakan Role enum untuk roles parameter
|
||||
use App\Enums\Role;
|
||||
use App\Services\NotificationService;
|
||||
|
||||
NotificationService::notify(
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Judul Notifikasi',
|
||||
body: 'Isi notifikasi',
|
||||
url: route('admin.module.index'),
|
||||
);
|
||||
|
||||
// ❌ JANGAN pakai string
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], // JANGAN
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
### Media Sync — Gunakan syncPhoto/syncReceipt
|
||||
```php
|
||||
// ✅ SELALU gunakan syncPhoto untuk photo_key, syncReceipt untuk receipt_key
|
||||
// Trait: App\Services\Concerns\RegistersMedia
|
||||
|
||||
// Photo — otomatis handle comparison + clear + register
|
||||
$this->syncPhoto($model, $data, 'photos');
|
||||
|
||||
// Receipt — otomatis handle comparison + cache clearing + clear + register
|
||||
$this->syncReceipt($model, $data['receipt_key'] ?? null, 'receipts', 'cache_prefix', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
|
||||
// ❌ JANGAN handle manual
|
||||
if (! empty($data['photo_key'])) {
|
||||
$model->clearMediaCollection('photos');
|
||||
$this->registerMedia($model, $data['photo_key'], 'photos'); // JANGAN
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
@ -1544,6 +1673,8 @@ ### ✅ Do
|
||||
- Selalu pindahkan logic ke Service (controller = lalu lintas saja)
|
||||
- Selalu pakai nama method yang konsisten di semua service (`paginated`, `getAll`, `store`, `update`, `destroy`)
|
||||
- Selalu cari kode berulang → jadikan Trait/Concern
|
||||
- Selalu pakai `Role` enum untuk role checking & notifikasi (bukan string)
|
||||
- Selalu pakai `syncPhoto`/`syncReceipt` untuk media handling (bukan manual)
|
||||
- Selalu pakai `withTrashed()` untuk relasi ke model yang mungkin di-soft-delete
|
||||
- Selalu pakai return type di SEMUA method (controller & service)
|
||||
- **Frontend**: Selalu gunakan `formatted_*` accessor dari model, bukan format di TypeScript
|
||||
@ -1557,6 +1688,8 @@ ### ❌ Don't
|
||||
- Jangan pakai `$fillable` (pakai `#[Guarded]`)
|
||||
- Jangan biarkan kolom tanpa cast
|
||||
- Jangan pakai string biasa untuk data opsi (pakai Enum)
|
||||
- Jangan pakai string untuk role checking (pakai `Role` enum)
|
||||
- Jangan handle media sync manual (pakai `syncPhoto`/`syncReceipt`)
|
||||
- Jangan lupa buat scope untuk data opsi, gunakan `#[Scope]` + return type `void`
|
||||
- Jangan pakai `public function` untuk scope (pakai `protected function`)
|
||||
- Jangan lupa tulis relasi 2 ARAH
|
||||
|
||||
@ -143,6 +143,7 @@ # Manual create
|
||||
- [ ] Method wajib: `paginated()`, `getAll()`, `store()`, `update()`, `destroy()`
|
||||
- [ ] Select kolom yang dibutuhkan + eager load relasi
|
||||
- [ ] Gunakan traits jika perlu: `HandlesCashTransactions`, `HasStockAdjustment`, `RegistersMedia`
|
||||
- [ ] Urutan method: CRUD → custom methods → private helpers (di paling bawah)
|
||||
|
||||
### 5. Form Request
|
||||
```bash
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\ProvidesEnumOptions;
|
||||
|
||||
enum Role: string
|
||||
{
|
||||
use ProvidesEnumOptions;
|
||||
|
||||
case DEVELOPER = 'developer';
|
||||
case OWNER = 'owner';
|
||||
@ -95,7 +92,6 @@ public function permissions(): array
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
|
||||
@ -139,8 +135,6 @@ public function permissions(): array
|
||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||
Permission::ANALYSIS_MARKETING_SALES,
|
||||
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::STOK_OPNAMES_VIEW,
|
||||
|
||||
Permission::ATTENDANCES_VIEW,
|
||||
@ -167,7 +161,6 @@ public function permissions(): array
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
Permission::PRODUCTS_DELETE,
|
||||
Permission::PRODUCTS_TOGGLE_STATUS,
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::ORDERS_VIEW,
|
||||
Permission::ORDERS_CREATE,
|
||||
@ -293,7 +286,6 @@ public function permissions(): array
|
||||
Permission::RAW_MATERIALS_CREATE,
|
||||
Permission::RAW_MATERIALS_UPDATE,
|
||||
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
@ -393,7 +385,6 @@ public function permissions(): array
|
||||
Permission::LEAVE_REQUESTS_DELETE,
|
||||
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::STOCKS_VIEW,
|
||||
|
||||
Permission::STOK_OPNAMES_VIEW,
|
||||
Permission::STOK_OPNAMES_CREATE,
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
use App\Http\Requests\Admin\Finance\CashTransactionRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
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 Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -15,14 +16,15 @@
|
||||
class CashAccountController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private CashAccountService $service
|
||||
private CashAccountService $cashAccountService,
|
||||
private CashTransactionService $cashTransactionService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
return Inertia::render('admin/finance/cash-account/index', [
|
||||
'cashAccount' => $this->service->get(),
|
||||
'transactions' => $this->service->paginatedTransactions(
|
||||
'cashAccount' => $this->cashAccountService->get(),
|
||||
'transactions' => $this->cashTransactionService->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
filters: $request->only(['type']),
|
||||
),
|
||||
@ -36,7 +38,7 @@ public function index(PaginatedRequest $request): Response
|
||||
public function deposit(CashTransactionRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->deposit($request->validated()),
|
||||
fn () => $this->cashTransactionService->deposit($request->validated()),
|
||||
'Deposit berhasil ditambahkan.',
|
||||
'admin.finance.cash-accounts.index'
|
||||
);
|
||||
@ -45,7 +47,7 @@ public function deposit(CashTransactionRequest $request): RedirectResponse
|
||||
public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->withdrawal($request->validated()),
|
||||
fn () => $this->cashTransactionService->withdrawal($request->validated()),
|
||||
'Withdrawal berhasil ditambahkan.',
|
||||
'admin.finance.cash-accounts.index'
|
||||
);
|
||||
@ -54,7 +56,7 @@ public function withdrawal(CashTransactionRequest $request): RedirectResponse
|
||||
public function update(CashTransactionRequest $request, CashTransaction $transaction): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->updateTransaction($transaction, $request->validated()),
|
||||
fn () => $this->cashTransactionService->update($transaction, $request->validated()),
|
||||
'Transaksi berhasil diperbarui.',
|
||||
'admin.finance.cash-accounts.index'
|
||||
);
|
||||
@ -63,7 +65,7 @@ public function update(CashTransactionRequest $request, CashTransaction $transac
|
||||
public function destroy(CashTransaction $transaction): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->deleteTransaction($transaction),
|
||||
fn () => $this->cashTransactionService->destroy($transaction),
|
||||
'Transaksi berhasil dihapus.',
|
||||
'admin.finance.cash-accounts.index'
|
||||
);
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollAdjustment;
|
||||
use App\Services\Admin\Finance\PayrollAdjustmentService;
|
||||
use App\Services\Admin\Finance\Payroll\PayrollAdjustmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class PayrollAdjustmentController extends Controller
|
||||
@ -18,7 +18,7 @@ public function __construct(
|
||||
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->store($payroll, $request->validated()),
|
||||
fn () => $this->service->store($payroll, $request->validated()),
|
||||
'Adjustment gaji berhasil ditambahkan.',
|
||||
'admin.finance.payroll-periods.show',
|
||||
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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->destroy($payrollAdjustment),
|
||||
fn () => $this->service->destroy($payrollAdjustment),
|
||||
'Adjustment gaji berhasil dihapus.',
|
||||
'admin.finance.payroll-periods.show',
|
||||
parameters: ['payroll_period' => $payrollAdjustment->payroll->payroll_period_id]
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Payroll;
|
||||
use App\Services\Admin\Finance\PayrollPeriodService;
|
||||
use App\Services\Admin\Finance\Payroll\PayrollPeriodService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class PayrollController extends Controller
|
||||
@ -16,7 +16,7 @@ public function __construct(
|
||||
public function pay(Payroll $payroll): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->pay($payroll),
|
||||
fn () => $this->service->pay($payroll),
|
||||
'Gaji berhasil dibayar.',
|
||||
'admin.finance.payroll-periods.show',
|
||||
parameters: ['payroll_period' => $payroll->payroll_period_id]
|
||||
@ -26,7 +26,7 @@ public function pay(Payroll $payroll): RedirectResponse
|
||||
public function cancel(Payroll $payroll): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->cancel($payroll),
|
||||
fn () => $this->service->cancel($payroll),
|
||||
'Gaji berhasil dibatalkan.',
|
||||
'admin.finance.payroll-periods.show',
|
||||
parameters: ['payroll_period' => $payroll->payroll_period_id]
|
||||
|
||||
@ -2,16 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance\Payroll;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\PayrollPeriod;
|
||||
use App\Services\Admin\Finance\PayrollPeriodService;
|
||||
use App\Services\Admin\Finance\Payroll\PayrollPeriodService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class PayrollPeriodController extends Controller
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
public function __construct(
|
||||
private PayrollPeriodService $service
|
||||
) {}
|
||||
@ -32,15 +36,23 @@ public function current(): RedirectResponse
|
||||
|
||||
public function show(PayrollPeriod $payrollPeriod): Response
|
||||
{
|
||||
$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', [
|
||||
'payrollPeriod' => $this->service->getDetail($payrollPeriod),
|
||||
'payrollPeriod' => $payrollPeriod,
|
||||
]);
|
||||
}
|
||||
|
||||
public function close(PayrollPeriod $payrollPeriod): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->close($payrollPeriod),
|
||||
fn () => $this->service->close($payrollPeriod),
|
||||
'Periode gaji berhasil ditutup.',
|
||||
'admin.finance.payroll-periods.index'
|
||||
);
|
||||
@ -49,7 +61,7 @@ public function close(PayrollPeriod $payrollPeriod): RedirectResponse
|
||||
public function reopen(PayrollPeriod $payrollPeriod): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->reopen($payrollPeriod),
|
||||
fn () => $this->service->reopen($payrollPeriod),
|
||||
'Periode gaji berhasil dibuka kembali.',
|
||||
'admin.finance.payroll-periods.index'
|
||||
);
|
||||
|
||||
@ -30,7 +30,7 @@ public function index(Request $request): Response
|
||||
public function store(AttendanceRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->checkIn($request->validated()),
|
||||
fn () => $this->service->checkIn($request->validated()),
|
||||
'Berhasil check-in.',
|
||||
'admin.hr.attendances.index'
|
||||
);
|
||||
@ -39,7 +39,7 @@ public function store(AttendanceRequest $request): RedirectResponse
|
||||
public function update(AttendanceRequest $request, Attendance $attendance): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->checkOut($attendance, $request->validated()),
|
||||
fn () => $this->service->checkOut($attendance, $request->validated()),
|
||||
'Berhasil check-out.',
|
||||
'admin.hr.attendances.index'
|
||||
);
|
||||
|
||||
@ -42,7 +42,7 @@ public function create(): Response
|
||||
public function store(EmployeeRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->store($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Pegawai berhasil ditambahkan.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -62,7 +62,7 @@ public function edit(User $user): Response
|
||||
public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($user, $request->validated()),
|
||||
fn () => $this->service->update($user, $request->validated()),
|
||||
'Pegawai berhasil diperbarui.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -71,7 +71,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->destroy($user),
|
||||
fn () => $this->service->destroy($user),
|
||||
'Pegawai berhasil dihapus.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -90,7 +90,7 @@ public function toggleActive(User $user): RedirectResponse
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->resetPassword($user),
|
||||
fn () => $this->service->resetPassword($user),
|
||||
'Kata sandi pegawai berhasil direset ke kata sandi default.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Cutting;
|
||||
use App\Services\Admin\Manage\CuttingService;
|
||||
use App\Services\Admin\Master\RawMaterial\RawMaterialVariantService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -15,6 +16,7 @@ class CuttingController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private CuttingService $service,
|
||||
private RawMaterialVariantService $rawMaterialVariantService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -29,14 +31,14 @@ public function index(PaginatedRequest $request): Response
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/cutting/create', [
|
||||
'data' => $this->service->getForCreate(),
|
||||
'rawMaterials' => $this->rawMaterialVariantService->getForCutting(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CuttingRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->create($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Cutting berhasil ditambahkan.',
|
||||
'admin.manage.cuttings.index',
|
||||
'admin.manage.cuttings.create'
|
||||
@ -47,7 +49,7 @@ public function edit(Cutting $cutting): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/cutting/edit', [
|
||||
'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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->delete($cutting),
|
||||
fn () => $this->service->destroy($cutting),
|
||||
'Cutting berhasil dihapus.',
|
||||
'admin.manage.cuttings.index'
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Purchase;
|
||||
use App\Services\Admin\Manage\PurchaseService;
|
||||
use App\Services\Admin\Master\RawMaterial\RawMaterialVariantService;
|
||||
use App\Services\Admin\Master\SupplierService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
@ -17,6 +18,7 @@ class PurchaseController extends Controller
|
||||
public function __construct(
|
||||
private readonly PurchaseService $service,
|
||||
private readonly SupplierService $supplierService,
|
||||
private readonly RawMaterialVariantService $rawMaterialVariantService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -34,14 +36,15 @@ public function index(PaginatedRequest $request): Response
|
||||
public function create(): Response
|
||||
{
|
||||
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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->create($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Belanja berhasil ditambahkan.',
|
||||
'admin.manage.purchases.index',
|
||||
'admin.manage.purchases.create'
|
||||
@ -52,7 +55,8 @@ public function edit(Purchase $purchase): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/purchase/edit', [
|
||||
'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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->delete($purchase),
|
||||
fn () => $this->service->destroy($purchase),
|
||||
'Belanja berhasil dihapus.',
|
||||
'admin.manage.purchases.index'
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Restock;
|
||||
use App\Services\Admin\Manage\RestockService;
|
||||
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -15,6 +16,7 @@ class RestockController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private RestockService $service,
|
||||
private ProductVariantService $productVariantService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -29,14 +31,14 @@ public function index(PaginatedRequest $request): Response
|
||||
public function create(): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/restock/create', [
|
||||
'data' => $this->service->getForCreate(),
|
||||
'products' => $this->productVariantService->getForRestock(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(RestockRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->create($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Restock berhasil ditambahkan.',
|
||||
'admin.manage.restocks.index',
|
||||
'admin.manage.restocks.create'
|
||||
@ -47,7 +49,7 @@ public function edit(Restock $restock): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/restock/edit', [
|
||||
'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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->service->delete($restock),
|
||||
fn () => $this->service->destroy($restock),
|
||||
'Restock berhasil dihapus.',
|
||||
'admin.manage.restocks.index'
|
||||
);
|
||||
|
||||
@ -2,11 +2,18 @@
|
||||
|
||||
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\Requests\Admin\Manage\TransactionRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\Manage\TransactionService;
|
||||
use App\Services\Admin\Master\CustomerService;
|
||||
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -15,6 +22,8 @@ class TransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private TransactionService $service,
|
||||
private ProductVariantService $productVariantService,
|
||||
private CustomerService $customerService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -35,14 +44,19 @@ public function index(PaginatedRequest $request): Response
|
||||
public function create(): Response
|
||||
{
|
||||
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
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->create($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Transaksi berhasil ditambahkan.',
|
||||
'admin.manage.transactions.index',
|
||||
'admin.manage.transactions.create'
|
||||
@ -53,14 +67,19 @@ public function edit(Order $transaction): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/transaction/edit', [
|
||||
'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(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(TransactionRequest $request, Order $transaction): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($transaction, $request->validated()),
|
||||
fn () => $this->service->update($transaction, $request->validated()),
|
||||
'Transaksi berhasil diperbarui.',
|
||||
'admin.manage.transactions.index',
|
||||
'admin.manage.transactions.edit',
|
||||
@ -71,7 +90,7 @@ public function update(TransactionRequest $request, Order $transaction): Redirec
|
||||
public function destroy(Order $transaction): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->delete($transaction),
|
||||
fn () => $this->service->destroy($transaction),
|
||||
'Transaksi berhasil dihapus.',
|
||||
'admin.manage.transactions.index'
|
||||
);
|
||||
@ -80,9 +99,21 @@ public function destroy(Order $transaction): RedirectResponse
|
||||
public function updateStatus(Order $transaction): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->updateStatus($transaction, request('status')),
|
||||
fn () => $this->service->updateStatus($transaction, request('status')),
|
||||
'Status transaksi berhasil diperbarui.',
|
||||
'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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ public function create(): Response
|
||||
public function store(ProductRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->create($request->validated()),
|
||||
fn () => $this->service->create($request->validated()),
|
||||
'Produk berhasil ditambahkan.',
|
||||
'admin.master.products.index',
|
||||
'admin.master.products.create'
|
||||
@ -60,7 +60,7 @@ public function edit(Product $product): Response
|
||||
public function update(ProductRequest $request, Product $product): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($product, $request->validated()),
|
||||
fn () => $this->service->update($product, $request->validated()),
|
||||
'Produk berhasil diperbarui.',
|
||||
'admin.master.products.index',
|
||||
'admin.master.products.edit',
|
||||
@ -71,7 +71,7 @@ public function update(ProductRequest $request, Product $product): RedirectRespo
|
||||
public function destroy(Product $product): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->delete($product),
|
||||
fn () => $this->service->delete($product),
|
||||
'Produk berhasil dihapus.',
|
||||
'admin.master.products.index'
|
||||
);
|
||||
|
||||
@ -36,7 +36,7 @@ public function create(): Response
|
||||
public function store(RawMaterialRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->store($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Bahan baku berhasil ditambahkan.',
|
||||
'admin.master.raw-materials.index',
|
||||
'admin.master.raw-materials.create'
|
||||
@ -53,7 +53,7 @@ public function edit(RawMaterial $rawMaterial): Response
|
||||
public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($rawMaterial, $request->validated()),
|
||||
fn () => $this->service->update($rawMaterial, $request->validated()),
|
||||
'Bahan baku berhasil diperbarui.',
|
||||
'admin.master.raw-materials.index',
|
||||
'admin.master.raw-materials.edit',
|
||||
@ -64,7 +64,7 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
|
||||
public function destroy(RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->destroy($rawMaterial),
|
||||
fn () => $this->service->destroy($rawMaterial),
|
||||
'Bahan baku berhasil dihapus.',
|
||||
'admin.master.raw-materials.index'
|
||||
);
|
||||
|
||||
@ -34,7 +34,7 @@ public function create(): Response
|
||||
public function store(RoleRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->store($request->validated()),
|
||||
fn () => $this->service->store($request->validated()),
|
||||
'Role berhasil ditambahkan.',
|
||||
'admin.settings.roles.index'
|
||||
);
|
||||
@ -51,7 +51,7 @@ public function edit(Role $role): Response
|
||||
public function update(RoleRequest $request, Role $role): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($role, $request->validated()),
|
||||
fn () => $this->service->update($role, $request->validated()),
|
||||
'Role berhasil diperbarui.',
|
||||
'admin.settings.roles.index'
|
||||
);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
use App\Services\AnalysisService;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -9,6 +11,8 @@
|
||||
|
||||
class AnalysisController extends Controller
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
public function __construct(
|
||||
private AnalysisService $service
|
||||
) {}
|
||||
@ -16,7 +20,7 @@ public function __construct(
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$isManager = $user->hasAnyRole(['owner', 'developer', 'admin-toko', 'direktur']);
|
||||
$isManager = self::hasAnyRole([Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO, Role::DIREKTUR]);
|
||||
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
@ -117,7 +117,7 @@ public function __invoke(Request $request): Response
|
||||
'category' => $category,
|
||||
],
|
||||
'seo' => [
|
||||
'title' => $system->app_name . ' - ' . $homepage->hero_badge,
|
||||
'title' => $system->app_name.' - '.$homepage->hero_badge,
|
||||
'description' => $homepage->hero_description,
|
||||
'image' => url('/assets/logo.png'),
|
||||
'url' => url('/'),
|
||||
|
||||
@ -40,9 +40,9 @@ public function rules(): array
|
||||
'category_ids' => ['required', 'array', 'min:1'],
|
||||
'category_ids.*' => [Rule::exists('categories', 'id')],
|
||||
'use_same_price' => ['nullable', 'boolean'],
|
||||
'shared_prices' => [Rule::requiredIf(fn() => $useSamePrice), 'nullable', 'array', ...($useSamePrice ? ['size:9'] : [])],
|
||||
'shared_prices.*.type' => [Rule::requiredIf(fn() => $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||
'shared_prices.*.price' => [Rule::requiredIf(fn() => $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||
'shared_prices' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', 'array', ...($useSamePrice ? ['size:9'] : [])],
|
||||
'shared_prices.*.type' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||
'shared_prices.*.price' => [Rule::requiredIf(fn () => $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||
'variants' => ['required', 'array', 'min:1'],
|
||||
'variants.*.id' => ['nullable', 'integer'],
|
||||
'variants.*.name' => ['required', 'string', 'max:200'],
|
||||
@ -51,10 +51,10 @@ public function rules(): array
|
||||
'variants.*.retail_stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.photo_keys' => ['required', 'array', 'min:1', 'max:5'],
|
||||
'variants.*.photo_keys.*' => ['required', 'string', 'max:500'],
|
||||
'variants.*.prices' => [Rule::requiredIf(fn() => ! $useSamePrice), 'nullable', 'array', ...(! $useSamePrice ? ['size:9'] : [])],
|
||||
'variants.*.prices' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', 'array', ...(! $useSamePrice ? ['size:9'] : [])],
|
||||
'variants.*.prices.*.id' => ['nullable', 'integer'],
|
||||
'variants.*.prices.*.type' => [Rule::requiredIf(fn() => ! $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||
'variants.*.prices.*.price' => [Rule::requiredIf(fn() => ! $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||
'variants.*.prices.*.type' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', Rule::in(PriceType::values())],
|
||||
'variants.*.prices.*.price' => [Rule::requiredIf(fn () => ! $useSamePrice), 'nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
@ -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
|
||||
{
|
||||
$settings = app(SocialMediaSettings::class);
|
||||
@ -167,4 +158,13 @@ public function updateHR(array $data): void
|
||||
$settings->fill($data);
|
||||
$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
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
namespace App\Services\Admin\Finance\Cash;
|
||||
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\Role;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
@ -11,45 +12,22 @@
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\LengthAwarePaginator as PaginationLengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CashAccountService
|
||||
class CashTransactionService
|
||||
{
|
||||
use HandlesCashTransactions, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
$cashAccount = $this->cashAccountService->get();
|
||||
|
||||
if (! $cashAccount) {
|
||||
return new PaginationLengthAwarePaginator(collect(), 0, $perPage);
|
||||
@ -70,29 +48,6 @@ public function paginatedTransactions(int $perPage = 25, string $search = '', st
|
||||
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
|
||||
{
|
||||
$transaction = DB::transaction(fn () => $this->creditCash(
|
||||
@ -100,12 +55,12 @@ public function deposit(array $data): CashTransaction
|
||||
description: $data['description'],
|
||||
));
|
||||
|
||||
if (! empty($data['receipt_key'])) {
|
||||
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
if (array_key_exists('receipt_key', $data)) {
|
||||
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Setoran Kas Toko',
|
||||
body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.cash-accounts.index'),
|
||||
@ -122,12 +77,12 @@ public function withdrawal(array $data): CashTransaction
|
||||
type: CashTransactionType::WITHDRAWAL,
|
||||
));
|
||||
|
||||
if (! empty($data['receipt_key'])) {
|
||||
$this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
if (array_key_exists('receipt_key', $data)) {
|
||||
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Penarikan Kas Toko',
|
||||
body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.cash-accounts.index'),
|
||||
@ -136,7 +91,7 @@ public function withdrawal(array $data): CashTransaction
|
||||
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) {
|
||||
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)) {
|
||||
$currentMedia = $transaction->getFirstMedia('receipts');
|
||||
$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);
|
||||
}
|
||||
}
|
||||
$this->syncReceipt($transaction, $data['receipt_key'] ?? null, 'receipts', 'cash_transaction_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Transaksi Kas Diperbarui',
|
||||
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.cash-accounts.index'),
|
||||
@ -197,7 +139,7 @@ public function updateTransaction(CashTransaction $transaction, array $data): Ca
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
public function deleteTransaction(CashTransaction $transaction): bool
|
||||
public function destroy(CashTransaction $transaction): bool
|
||||
{
|
||||
return DB::transaction(function () use ($transaction) {
|
||||
if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) {
|
||||
@ -221,13 +163,18 @@ public function deleteTransaction(CashTransaction $transaction): bool
|
||||
|
||||
$cashAccount->update(['balance' => $newBalance]);
|
||||
|
||||
$media = $transaction->getFirstMedia('receipts');
|
||||
if ($media) {
|
||||
Cache::forget("cash_transaction_receipt_{$media->id}");
|
||||
}
|
||||
|
||||
$transaction->clearMediaCollection('receipts');
|
||||
|
||||
$deleted = $transaction->delete();
|
||||
|
||||
if ($deleted) {
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Transaksi Kas Dihapus',
|
||||
body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.cash-accounts.index'),
|
||||
@ -237,4 +184,27 @@ public function deleteTransaction(CashTransaction $transaction): bool
|
||||
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,41 +2,28 @@
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\Role;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Models\EmployeeAdvancePayment;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EmployeeAdvanceService
|
||||
{
|
||||
use HandlesCashTransactions;
|
||||
|
||||
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();
|
||||
}
|
||||
use HandlesCashTransactions, HasRoleChecks;
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return EmployeeAdvance::query()
|
||||
->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())))
|
||||
->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($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
@ -64,7 +51,7 @@ public function store(array $data): EmployeeAdvance
|
||||
$employeeAdvance->load('employee.user');
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Kasbon Baru',
|
||||
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'),
|
||||
@ -181,7 +168,7 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Kasbon Disetujui',
|
||||
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah disetujui'.' oleh '.auth()->user()->full_name.'.',
|
||||
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, ',', '.').'.';
|
||||
|
||||
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',
|
||||
body: $notificationBody,
|
||||
url: route('admin.finance.employee-advances.index'),
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Admin\Finance;
|
||||
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\Role;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\Expense;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
@ -10,7 +11,6 @@
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -23,15 +23,6 @@ public function __construct(
|
||||
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
|
||||
{
|
||||
$paginator = Expense::query()
|
||||
@ -46,35 +37,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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 store(array $data): Expense
|
||||
{
|
||||
$expense = DB::transaction(function () use ($data) {
|
||||
@ -90,15 +52,15 @@ public function store(array $data): Expense
|
||||
'description' => $data['description'],
|
||||
]);
|
||||
|
||||
if (! empty($data['receipt_key'])) {
|
||||
$this->registerMedia($expense, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
if (array_key_exists('receipt_key', $data)) {
|
||||
$this->syncReceipt($expense, $data['receipt_key'] ?? null, 'receipts', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
}
|
||||
|
||||
return $expense;
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengeluaran Baru',
|
||||
body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.',
|
||||
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)) {
|
||||
$currentMedia = $expense->getFirstMedia('receipts');
|
||||
$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);
|
||||
}
|
||||
}
|
||||
$this->syncReceipt($expense, $data['receipt_key'] ?? null, 'receipts', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null);
|
||||
}
|
||||
|
||||
return $expense;
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengeluaran Diperbarui',
|
||||
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.expenses.index'),
|
||||
@ -182,7 +125,6 @@ public function destroy(Expense $expense): bool
|
||||
type: CashTransactionType::DEPOSIT,
|
||||
);
|
||||
|
||||
// Invalidate receipt cache
|
||||
$media = $expense->getFirstMedia('receipts');
|
||||
if ($media) {
|
||||
Cache::forget("expense_receipt_{$media->id}");
|
||||
@ -194,7 +136,7 @@ public function destroy(Expense $expense): bool
|
||||
|
||||
if ($deleted) {
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengeluaran Dihapus',
|
||||
body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.expenses.index'),
|
||||
@ -204,4 +146,32 @@ public function destroy(Expense $expense): bool
|
||||
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
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
namespace App\Services\Admin\Finance\Payroll;
|
||||
|
||||
use App\Enums\PayrollAdjustmentType;
|
||||
use App\Enums\PayrollStatus;
|
||||
@ -1,67 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
namespace App\Services\Admin\Finance\Payroll;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\PayrollPeriodStatus;
|
||||
use App\Enums\PayrollStatus;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollPeriod;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PayrollPeriodService
|
||||
{
|
||||
use HandlesCashTransactions;
|
||||
|
||||
private function canViewAll(): bool
|
||||
{
|
||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
||||
}
|
||||
|
||||
public function getCurrentOrCreate(): PayrollPeriod
|
||||
{
|
||||
$now = now();
|
||||
|
||||
return PayrollPeriod::firstOrCreate(
|
||||
['year' => $now->year, 'month' => $now->month],
|
||||
['status' => PayrollPeriodStatus::OPEN]
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
use HandlesCashTransactions, HasRoleChecks;
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return PayrollPeriod::query()
|
||||
->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()))])
|
||||
->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')
|
||||
@ -69,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 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')
|
||||
->withSum('payrolls', 'total_amount')
|
||||
->withSum('payrolls', 'bonus_amount')
|
||||
@ -82,15 +44,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getDetail(PayrollPeriod $period): PayrollPeriod
|
||||
public function getCurrentOrCreate(): PayrollPeriod
|
||||
{
|
||||
return $period->load([
|
||||
'payrolls' => function ($query) {
|
||||
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
|
||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||
->orderBy('id');
|
||||
},
|
||||
]);
|
||||
$now = now();
|
||||
|
||||
return PayrollPeriod::firstOrCreate(
|
||||
['year' => $now->year, 'month' => $now->month],
|
||||
['status' => PayrollPeriodStatus::OPEN]
|
||||
);
|
||||
}
|
||||
|
||||
public function close(PayrollPeriod $period): PayrollPeriod
|
||||
@ -171,7 +132,7 @@ public function pay(Payroll $payroll): Payroll
|
||||
$employeeUser = $payroll->employee->user ?? null;
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Gaji Dibayar',
|
||||
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibayar".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.payroll-periods.index'),
|
||||
@ -202,7 +163,7 @@ public function cancel(Payroll $payroll): Payroll
|
||||
$employeeUser = $payroll->employee->user ?? null;
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Gaji Dibatalkan',
|
||||
body: "Gaji karyawan {$payroll->employee->name} sebesar {$payroll->formatted_amount} telah dibatalkan".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.finance.payroll-periods.index'),
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services\Admin\HR;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Models\LeaveRequest;
|
||||
@ -17,16 +19,17 @@
|
||||
|
||||
class AttendanceService
|
||||
{
|
||||
use RegistersMedia;
|
||||
use HasRoleChecks, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private S3PresignedService $s3Service,
|
||||
private EmployeeService $employeeService,
|
||||
) {}
|
||||
|
||||
public function getIndexData(int $year, int $month): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
$isAdmin = $user->hasAnyRole(['developer', 'owner']);
|
||||
$isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER]);
|
||||
$hrSettings = app(HRSettings::class);
|
||||
|
||||
$employeeId = $isAdmin ? null : $user->employee?->id;
|
||||
@ -34,7 +37,7 @@ public function getIndexData(int $year, int $month): array
|
||||
return [
|
||||
'attendances' => $this->getByMonth($year, $month, $employeeId),
|
||||
'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
|
||||
'employees' => $isAdmin ? $this->getAllEmployees() : [],
|
||||
'employees' => $isAdmin ? $this->employeeService->getAll() : [],
|
||||
'todayAttendance' => $isAdmin ? null : $this->getToday(),
|
||||
'currentYear' => $year,
|
||||
'currentMonth' => $month,
|
||||
@ -49,14 +52,6 @@ public function getIndexData(int $year, int $month): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Attendance::with(['employee.user.userProfile', 'media'])
|
||||
->latest('attendance_date')
|
||||
->get()
|
||||
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
||||
}
|
||||
|
||||
public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
||||
{
|
||||
return Attendance::with(['employee.user.userProfile', 'media'])
|
||||
@ -82,21 +77,6 @@ public function getToday(): ?array
|
||||
return $this->getByDate(now()->toDateString());
|
||||
}
|
||||
|
||||
public function isOnLeave(User $user): bool
|
||||
{
|
||||
$employee = $user->employee;
|
||||
|
||||
if (! $employee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LeaveRequest::approved()
|
||||
->where('employee_id', $employee->id)
|
||||
->where('start_date', '<=', now()->toDateString())
|
||||
->where('end_date', '>=', now()->toDateString())
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null): Collection
|
||||
{
|
||||
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||
@ -119,17 +99,6 @@ public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null)
|
||||
]);
|
||||
}
|
||||
|
||||
public function getAllEmployees(): 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 getMonthStats(int $year, int $month, ?int $employeeId = null): array
|
||||
{
|
||||
$startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||
@ -153,6 +122,87 @@ public function getMonthStats(int $year, int $month, ?int $employeeId = null): a
|
||||
return $this->getMonthStatsForAll($year, $month, $startOfMonth, $statEnd, $workingDays);
|
||||
}
|
||||
|
||||
public function isOnLeave(User $user): bool
|
||||
{
|
||||
$employee = $user->employee;
|
||||
|
||||
if (! $employee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LeaveRequest::approved()
|
||||
->where('employee_id', $employee->id)
|
||||
->where('start_date', '<=', now()->toDateString())
|
||||
->where('end_date', '>=', now()->toDateString())
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function checkIn(array $data): Attendance
|
||||
{
|
||||
$employee = auth()->user()->employee;
|
||||
|
||||
if (! $employee) {
|
||||
throw ValidationException::withMessages([
|
||||
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
|
||||
]);
|
||||
}
|
||||
|
||||
$today = now()->toDateString();
|
||||
|
||||
$existing = Attendance::where('employee_id', $employee->id)
|
||||
->where('attendance_date', $today)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
throw ValidationException::withMessages([
|
||||
'attendance' => 'Anda sudah melakukan presensi hari ini.',
|
||||
]);
|
||||
}
|
||||
|
||||
$attendance = Attendance::create([
|
||||
'employee_id' => $employee->id,
|
||||
'attendance_date' => $today,
|
||||
'check_in_at' => now(),
|
||||
'check_in_latitude' => $data['latitude'],
|
||||
'check_in_longitude' => $data['longitude'],
|
||||
]);
|
||||
|
||||
if (! empty($data['photo'])) {
|
||||
$this->registerMediaFromBase64($attendance, $data['photo'], 'check-in', 'attendances');
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||
title: 'Presensi Masuk',
|
||||
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.attendances.index'),
|
||||
);
|
||||
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
public function checkOut(Attendance $attendance, array $data): Attendance
|
||||
{
|
||||
$attendance->update([
|
||||
'check_out_at' => now(),
|
||||
'check_out_latitude' => $data['latitude'],
|
||||
'check_out_longitude' => $data['longitude'],
|
||||
]);
|
||||
|
||||
if (! empty($data['photo'])) {
|
||||
$this->registerMediaFromBase64($attendance, $data['photo'], 'check-out', 'attendances');
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||
title: 'Presensi Pulang',
|
||||
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.attendances.index'),
|
||||
);
|
||||
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays, int $employeeId): array
|
||||
{
|
||||
$attendanceQuery = Attendance::whereYear('attendance_date', $year)
|
||||
@ -240,72 +290,6 @@ private function getMonthStatsForAll(int $year, int $month, Carbon $startOfMonth
|
||||
];
|
||||
}
|
||||
|
||||
public function checkIn(array $data): Attendance
|
||||
{
|
||||
$employee = auth()->user()->employee;
|
||||
|
||||
if (! $employee) {
|
||||
throw ValidationException::withMessages([
|
||||
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
|
||||
]);
|
||||
}
|
||||
|
||||
$today = now()->toDateString();
|
||||
|
||||
$existing = Attendance::where('employee_id', $employee->id)
|
||||
->where('attendance_date', $today)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
throw ValidationException::withMessages([
|
||||
'attendance' => 'Anda sudah melakukan presensi hari ini.',
|
||||
]);
|
||||
}
|
||||
|
||||
$attendance = Attendance::create([
|
||||
'employee_id' => $employee->id,
|
||||
'attendance_date' => $today,
|
||||
'check_in_at' => now(),
|
||||
'check_in_latitude' => $data['latitude'],
|
||||
'check_in_longitude' => $data['longitude'],
|
||||
]);
|
||||
|
||||
if (! empty($data['photo'])) {
|
||||
$this->registerMediaFromBase64($attendance, $data['photo'], 'check-in', 'attendances');
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur'],
|
||||
title: 'Presensi Masuk',
|
||||
body: 'Presensi masuk oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.attendances.index'),
|
||||
);
|
||||
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
public function checkOut(Attendance $attendance, array $data): Attendance
|
||||
{
|
||||
$attendance->update([
|
||||
'check_out_at' => now(),
|
||||
'check_out_latitude' => $data['latitude'],
|
||||
'check_out_longitude' => $data['longitude'],
|
||||
]);
|
||||
|
||||
if (! empty($data['photo'])) {
|
||||
$this->registerMediaFromBase64($attendance, $data['photo'], 'check-out', 'attendances');
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur'],
|
||||
title: 'Presensi Pulang',
|
||||
body: 'Presensi pulang oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.attendances.index'),
|
||||
);
|
||||
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
private function formatAttendance(Attendance $attendance): array
|
||||
{
|
||||
$toArray = $attendance->toArray();
|
||||
|
||||
@ -2,46 +2,17 @@
|
||||
|
||||
namespace App\Services\Admin\HR;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmployeeService
|
||||
{
|
||||
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
|
||||
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();
|
||||
}
|
||||
use HasRoleChecks;
|
||||
|
||||
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']),
|
||||
'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');
|
||||
$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($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)))
|
||||
@ -66,6 +37,17 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -2,40 +2,26 @@
|
||||
|
||||
namespace App\Services\Admin\HR;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Enums\Role;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Services\NotificationService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LeaveRequestService
|
||||
{
|
||||
private function canViewAll(): bool
|
||||
{
|
||||
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();
|
||||
}
|
||||
use HasRoleChecks;
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return LeaveRequest::query()
|
||||
->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(! 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($filters['status'] ?? null, function ($query, $status) {
|
||||
$query->where('status', $status);
|
||||
@ -71,7 +57,7 @@ public function store(array $data): LeaveRequest
|
||||
$leaveRequest->load('employee.user');
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Pengajuan Cuti Baru',
|
||||
body: "Pengajuan cuti {$leaveRequest->total_days} hari oleh ".auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.leave-requests.index'),
|
||||
@ -114,7 +100,7 @@ public function approve(LeaveRequest $leaveRequest): LeaveRequest
|
||||
$employeeUser = $leaveRequest->employee->user ?? null;
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Cuti Disetujui',
|
||||
body: "Cuti {$leaveRequest->employee->name} telah disetujui oleh ".auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.leave-requests.index'),
|
||||
@ -135,7 +121,7 @@ public function reject(LeaveRequest $leaveRequest): LeaveRequest
|
||||
$employeeUser = $leaveRequest->employee->user ?? null;
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Cuti Ditolak',
|
||||
body: "Cuti {$leaveRequest->employee->name} telah ditolak oleh ".auth()->user()->full_name.'.',
|
||||
url: route('admin.hr.leave-requests.index'),
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
use App\Models\CuttingMaterialCombination;
|
||||
use App\Models\CuttingResult;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Admin\Master\RawMaterial\RawMaterialService;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@ -20,7 +19,6 @@ class CuttingService
|
||||
|
||||
public function __construct(
|
||||
private S3PresignedService $s3Service,
|
||||
private RawMaterialService $rawMaterialService,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||
@ -62,13 +60,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function getForCreate(): array
|
||||
{
|
||||
return [
|
||||
'rawMaterials' => $this->rawMaterialService->getVariantsForCutting(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getForEdit(Cutting $cutting): array
|
||||
{
|
||||
$cutting->load([
|
||||
@ -129,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) {
|
||||
foreach ($data['materials'] as $materialData) {
|
||||
@ -335,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) {
|
||||
$cutting->load('cuttingMaterials.rawMaterialPrice');
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Admin\Manage;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Purchase;
|
||||
use App\Models\PurchaseItem;
|
||||
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') {
|
||||
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) {
|
||||
$now = now();
|
||||
@ -219,7 +220,7 @@ private function createFromExisting(array $data): Purchase
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Admin Bahan Baku'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Belanja Baru',
|
||||
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||
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) {
|
||||
$rawMaterial = RawMaterial::create([
|
||||
@ -309,7 +310,7 @@ private function createNew(array $data): Purchase
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Admin Bahan Baku'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
||||
title: 'Belanja Baru',
|
||||
body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.manage.purchases.index'),
|
||||
@ -460,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) {
|
||||
$purchase->load('purchaseItems.rawMaterialPrice');
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Enums\PriceType;
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\Restock;
|
||||
@ -30,14 +31,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->with([
|
||||
'createdBy:id',
|
||||
'createdBy.userProfile:id,user_id,full_name',
|
||||
'restockItems' => fn ($q) => $q
|
||||
'restockItems' => fn($q) => $q
|
||||
->select(['id', 'restock_id', 'product_variant_id', 'quantity', 'unit_price', 'subtotal'])
|
||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
||||
'restockItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
'restockItems.productVariant.product:id,name',
|
||||
])
|
||||
->when($search, function ($q) use ($search) {
|
||||
$q->whereHas('restockItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||
$q->whereHas('restockItems.productVariant.product', fn($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||
->orWhere('notes', 'like', "%{$search}%");
|
||||
})
|
||||
->orderBy($sort, $direction)
|
||||
@ -59,65 +60,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function getForCreate(): array
|
||||
{
|
||||
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
|
||||
public function store(array $data): Restock
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$now = now();
|
||||
@ -143,9 +86,9 @@ public function create(array $data): Restock
|
||||
$this->syncPhoto($restock, $data);
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
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'),
|
||||
);
|
||||
|
||||
@ -189,7 +132,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) {
|
||||
$restock->load('restockItems');
|
||||
@ -219,7 +162,7 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt
|
||||
->get()
|
||||
->mapWithKeys(function (ProductVariant $variant) use ($priceType) {
|
||||
$price = $variant->productPrices
|
||||
->first(fn ($p) => $p->type === $priceType);
|
||||
->first(fn($p) => $p->type === $priceType);
|
||||
|
||||
return [$variant->id => $price?->price ?? 0];
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Enums\PaymentType;
|
||||
use App\Enums\PriceType;
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
@ -143,83 +144,7 @@ public function getFilterOptions(): array
|
||||
];
|
||||
}
|
||||
|
||||
public function getForCreate(): array
|
||||
{
|
||||
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
|
||||
public function store(array $data): Order
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$now = now();
|
||||
@ -268,7 +193,7 @@ public function create(array $data): Order
|
||||
}
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
|
||||
title: 'Transaksi Baru',
|
||||
body: 'Transaksi ' . $order->order_number . ' sebesar Rp ' . number_format($totalAmount, 0, ',', '.') . ' berhasil dicatat oleh ' . auth()->user()->full_name . '.',
|
||||
url: route('admin.manage.transactions.index'),
|
||||
@ -339,7 +264,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) {
|
||||
$order->load('orderItems');
|
||||
|
||||
@ -8,20 +8,20 @@
|
||||
|
||||
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
|
||||
{
|
||||
return Category::query()
|
||||
->select(['id', 'name'])
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Category::select(['id', 'name'])->latest()->get();
|
||||
}
|
||||
|
||||
public function store(array $data): Category
|
||||
{
|
||||
return Category::create($data);
|
||||
|
||||
@ -8,11 +8,6 @@
|
||||
|
||||
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
|
||||
{
|
||||
return Customer::query()
|
||||
@ -22,6 +17,11 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Customer::select(['id', 'name'])->orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function store(array $data): Customer
|
||||
{
|
||||
return Customer::create($data);
|
||||
|
||||
@ -2,75 +2,28 @@
|
||||
|
||||
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\ProductVariant;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\S3PresignedService;
|
||||
use App\Services\StockMutationService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ProductService
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
public function __construct(
|
||||
private ProductVariantService $variantService,
|
||||
private S3PresignedService $s3Service,
|
||||
private StockMutationService $stockMutationService,
|
||||
) {}
|
||||
|
||||
private function canVerify(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->hasAnyRole(['developer', 'owner']);
|
||||
}
|
||||
|
||||
private function assertNotPending(Product $product): void
|
||||
{
|
||||
if ($product->status === ProductStatus::PENDING && ! $this->canVerify()) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => 'Produk sedang menunggu verifikasi dan tidak dapat diubah.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getNames(): array
|
||||
{
|
||||
return Product::where('status', '!=', 'deleted')
|
||||
->orderBy('name')
|
||||
->pluck('name')
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
$products = Product::select(['id', 'name', 'slug', 'description', 'status', 'rejection_reason'])
|
||||
->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();
|
||||
|
||||
$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
|
||||
{
|
||||
$paginator = Product::query()
|
||||
@ -105,9 +58,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function create(array $data): Product
|
||||
public function store(array $data): Product
|
||||
{
|
||||
$status = $this->canVerify()
|
||||
$status = self::hasAnyRole([Role::DEVELOPER, Role::OWNER])
|
||||
? ($data['status'] ?? ProductStatus::ACTIVE)
|
||||
: ProductStatus::PENDING;
|
||||
|
||||
@ -182,7 +135,7 @@ public function create(array $data): Product
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Produk Baru',
|
||||
body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
@ -236,7 +189,7 @@ public function update(Product $product, array $data): Product
|
||||
$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 && ! $this->canVerify()) {
|
||||
if ($product->status === ProductStatus::REJECTED && ! self::hasAnyRole([Role::DEVELOPER, Role::OWNER])) {
|
||||
$newStatus = ProductStatus::PENDING;
|
||||
}
|
||||
|
||||
@ -433,7 +386,7 @@ public function update(Product $product, array $data): Product
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Produk Diperbarui',
|
||||
body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
@ -442,7 +395,7 @@ public function update(Product $product, array $data): Product
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function delete(Product $product): bool
|
||||
public function destroy(Product $product): bool
|
||||
{
|
||||
$this->assertNotPending($product);
|
||||
|
||||
@ -459,7 +412,7 @@ public function delete(Product $product): bool
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Produk Dihapus',
|
||||
body: "Produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
@ -484,7 +437,7 @@ public function approve(Product $product): void
|
||||
]);
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer'],
|
||||
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'),
|
||||
@ -500,7 +453,7 @@ public function reject(Product $product, string $reason = ''): void
|
||||
]);
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer'],
|
||||
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'),
|
||||
@ -516,10 +469,19 @@ public function resubmit(Product $product): void
|
||||
]);
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer'],
|
||||
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,7 +2,10 @@
|
||||
|
||||
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\ProductVariant;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
@ -14,27 +17,64 @@
|
||||
|
||||
class ProductVariantService
|
||||
{
|
||||
use RegistersMedia;
|
||||
use HasRoleChecks, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private S3PresignedService $s3Service,
|
||||
private StockMutationService $stockMutationService,
|
||||
) {}
|
||||
|
||||
private function canVerify(): bool
|
||||
public function getForRestock(): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
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;
|
||||
|
||||
return $user->hasAnyRole(['developer', 'owner']);
|
||||
$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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function assertNotPending(Product $product): void
|
||||
public function getForTransaction(): array
|
||||
{
|
||||
if ($product->status === ProductStatus::PENDING && ! $this->canVerify()) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => 'Produk sedang menunggu verifikasi dan tidak dapat diubah.',
|
||||
]);
|
||||
}
|
||||
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
|
||||
@ -102,7 +142,7 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Diperbarui',
|
||||
body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
@ -111,7 +151,7 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
||||
return $variant->fresh();
|
||||
}
|
||||
|
||||
public function delete(Product $product, ProductVariant $variant): bool
|
||||
public function destroy(Product $product, ProductVariant $variant): bool
|
||||
{
|
||||
$this->assertNotPending($product);
|
||||
|
||||
@ -123,7 +163,7 @@ public function delete(Product $product, ProductVariant $variant): bool
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Dihapus',
|
||||
body: "Varian \"{$variant->name}\" dari produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
@ -184,7 +224,7 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Transfer Stok',
|
||||
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'),
|
||||
@ -192,4 +232,13 @@ public function transferStock(ProductVariant $variant, array $data): ProductVari
|
||||
|
||||
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\S3PresignedService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RawMaterialService
|
||||
@ -18,27 +17,6 @@ public function __construct(
|
||||
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
|
||||
{
|
||||
$paginator = RawMaterial::query()
|
||||
@ -239,26 +217,6 @@ public function toggleStatus(RawMaterial $rawMaterial): void
|
||||
]);
|
||||
}
|
||||
|
||||
public function getVariantsForCutting(): 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function registerPhoto(RawMaterialPrice $price, string $s3Key): void
|
||||
{
|
||||
$this->registerMedia(
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Admin\Master\RawMaterial;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
@ -17,6 +18,26 @@ public function __construct(
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function getForEdit(RawMaterialPrice $variant): array
|
||||
{
|
||||
$variant->load('media');
|
||||
@ -61,7 +82,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Diperbarui',
|
||||
body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.raw-materials.index'),
|
||||
@ -70,7 +91,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice
|
||||
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) {
|
||||
$variant->clearMediaCollection('photos');
|
||||
@ -79,7 +100,7 @@ public function delete(RawMaterial $rawMaterial, RawMaterialPrice $variant): boo
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO],
|
||||
title: 'Varian Dihapus',
|
||||
body: "Varian \"{$variant->variant}\" dari bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.raw-materials.index'),
|
||||
|
||||
@ -8,11 +8,6 @@
|
||||
|
||||
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
|
||||
{
|
||||
return Supplier::query()
|
||||
@ -22,6 +17,11 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Supplier::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
||||
}
|
||||
|
||||
public function store(array $data): Supplier
|
||||
{
|
||||
return Supplier::create($data);
|
||||
|
||||
@ -2,17 +2,21 @@
|
||||
|
||||
namespace App\Services\Admin\Settings;
|
||||
|
||||
use App\Concerns\HasRoleChecks;
|
||||
use App\Enums\Role;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
use HasRoleChecks;
|
||||
|
||||
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')
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
@ -23,7 +27,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
public function store(array $data): Role
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$role = Role::create(['name' => $data['name']]);
|
||||
$role = SpatieRole::create(['name' => $data['name']]);
|
||||
$role->syncPermissions($data['permissions']);
|
||||
|
||||
return $role;
|
||||
@ -55,15 +59,15 @@ public function getPermissionsByModule(): array
|
||||
|
||||
public function getForEmployee(): Collection
|
||||
{
|
||||
$query = Role::where('name', '!=', 'Developer');
|
||||
$query = SpatieRole::where('name', '!=', Role::DEVELOPER->value);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->hasAnyRole(['admin-toko', 'direktur'])) {
|
||||
$query->where('name', '!=', 'admin-bahan-baku');
|
||||
if (self::hasAnyRole([Role::ADMIN_TOKO, Role::DIREKTUR])) {
|
||||
$query->where('name', '!=', Role::ADMIN_BAHAN_BAKU->value);
|
||||
}
|
||||
|
||||
if (! $user->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko'])) {
|
||||
if (! self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::DIREKTUR, Role::ADMIN_TOKO])) {
|
||||
$userRoles = $user->roles->pluck('name');
|
||||
$query->whereIn('name', $userRoles);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ public function getAttendanceStats(?string $startDate, ?string $endDate): array
|
||||
$current->addDay();
|
||||
}
|
||||
|
||||
$totalEmployees = Employee::whereHas('user', fn($q) => $q->where('is_active', true))->count();
|
||||
$totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count();
|
||||
|
||||
$present = Attendance::whereBetween('attendance_date', [$start, $end])->count();
|
||||
|
||||
@ -142,12 +142,12 @@ public function getRawMaterialStock(): array
|
||||
->get();
|
||||
|
||||
$totalStock = $prices->sum('stock');
|
||||
$totalValue = $prices->sum(fn($p) => $p->stock * $p->price);
|
||||
$totalValue = $prices->sum(fn ($p) => $p->stock * $p->price);
|
||||
|
||||
$byUnit = [
|
||||
'yard' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'),
|
||||
'meter' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'),
|
||||
'kilogram' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'),
|
||||
'yard' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'),
|
||||
'meter' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'),
|
||||
'kilogram' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'),
|
||||
];
|
||||
|
||||
return [
|
||||
@ -226,7 +226,7 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate): array
|
||||
->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) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction']));
|
||||
->map(fn ($item) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction']));
|
||||
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
@ -244,7 +244,7 @@ public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate)
|
||||
->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) => $item->only(['month', 'store', 'shopee', 'tiktok']));
|
||||
->map(fn ($item) => $item->only(['month', 'store', 'shopee', 'tiktok']));
|
||||
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
@ -259,7 +259,7 @@ public function getRevenueByPaymentType(?string $startDate, ?string $endDate): a
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||
->groupBy('payment_type')
|
||||
->get()
|
||||
->map(fn($item) => [
|
||||
->map(fn ($item) => [
|
||||
'payment_type' => $item->payment_type,
|
||||
'label' => $item->payment_type->label(),
|
||||
'total' => (int) $item->total,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
@ -109,4 +110,35 @@ private function syncPhoto(Model $model, array $data, string $collectionName = '
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncReceipt(Model $model, ?string $newKey, string $collectionName = 'receipts', string $cachePrefix = 'receipt', ?int $fileSize = null, ?string $fileMimeType = null): void
|
||||
{
|
||||
$currentMedia = $model->getFirstMedia($collectionName);
|
||||
$currentKey = $currentMedia?->file_name;
|
||||
|
||||
if ($currentMedia && ! str_contains($currentKey, '/')) {
|
||||
$currentKey = $currentMedia->getPath();
|
||||
}
|
||||
|
||||
if ($newKey === $currentKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($currentMedia) {
|
||||
Cache::forget("{$cachePrefix}_{$currentMedia->id}");
|
||||
}
|
||||
|
||||
$model->clearMediaCollection($collectionName);
|
||||
|
||||
if (! empty($newKey)) {
|
||||
$this->registerMedia(
|
||||
model: $model,
|
||||
s3Key: $newKey,
|
||||
collectionName: $collectionName,
|
||||
generatedConversions: ['thumb' => true],
|
||||
fileSize: $fileSize,
|
||||
mimeType: $fileMimeType,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,18 +2,24 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use App\Notifications\WebPushNotification;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NotificationService
|
||||
{
|
||||
/**
|
||||
* @param array<Role> $roles
|
||||
*/
|
||||
public static function notify(array $roles, string $title, string $body, string $url, ?User $additionalUser = null): void
|
||||
{
|
||||
$roleLabels = array_map(fn (Role $role) => $role->label(), $roles);
|
||||
|
||||
$users = User::query()
|
||||
->select(['id'])
|
||||
->where('is_active', true)
|
||||
->whereHas('roles', fn ($q) => $q->whereIn('name', $roles))
|
||||
->whereHas('roles', fn ($q) => $q->whereIn('name', $roleLabels))
|
||||
->get();
|
||||
|
||||
if ($additionalUser && ! $users->contains('id', $additionalUser->id)) {
|
||||
|
||||
@ -17,6 +17,16 @@ class StockMutationService
|
||||
'retail_stock' => 'retail',
|
||||
];
|
||||
|
||||
public function paginated(Model $model, int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
return StockMutation::query()
|
||||
->where('stockable_type', get_class($model))
|
||||
->where('stockable_id', $model->id)
|
||||
->with('user:id,username,email')
|
||||
->latest()
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
public function recordInitial(Model $model, array $stockData, string $description = 'Stok awal'): void
|
||||
{
|
||||
$userId = auth()->id();
|
||||
@ -172,14 +182,4 @@ public function recordTransfer(
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function paginated(Model $model, int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
return StockMutation::query()
|
||||
->where('stockable_type', get_class($model))
|
||||
->where('stockable_id', $model->id)
|
||||
->with('user:id,username,email')
|
||||
->latest()
|
||||
->paginate($perPage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
|
||||
@ -41,11 +41,10 @@ type CombinationState = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: CuttingCreateData;
|
||||
rawMaterials: CuttingCreateData['rawMaterials'];
|
||||
};
|
||||
|
||||
export default function CuttingCreate({ data }: Props) {
|
||||
const { rawMaterials } = data;
|
||||
export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
const { auth, errors } = usePage().props as { auth: { user?: { id?: number } }; errors: Record<string, string> };
|
||||
const userId = auth.user?.id;
|
||||
|
||||
|
||||
@ -42,11 +42,10 @@ type CombinationState = {
|
||||
|
||||
type Props = {
|
||||
cutting: CuttingForEdit;
|
||||
data: CuttingCreateData;
|
||||
rawMaterials: CuttingCreateData['rawMaterials'];
|
||||
};
|
||||
|
||||
export default function CuttingEdit({ cutting, data }: Props) {
|
||||
const { rawMaterials } = data;
|
||||
export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
const { errors } = usePage().props as { errors: Record<string, string> };
|
||||
|
||||
const [materials, setMaterials] = useState<MaterialState[]>(() => {
|
||||
|
||||
@ -72,11 +72,11 @@ type CartLine = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: PurchaseCreateData;
|
||||
suppliers: PurchaseCreateData['suppliers'];
|
||||
rawMaterials: PurchaseCreateData['rawMaterials'];
|
||||
};
|
||||
|
||||
export default function PurchaseCreate({ data }: Props) {
|
||||
const { suppliers, rawMaterials } = data;
|
||||
export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||
const userId = auth.user?.id;
|
||||
|
||||
|
||||
@ -57,11 +57,15 @@ type CartLine = {
|
||||
|
||||
type Props = {
|
||||
purchase: PurchaseForEdit;
|
||||
data: PurchaseCreateData;
|
||||
suppliers: PurchaseCreateData['suppliers'];
|
||||
rawMaterials: PurchaseCreateData['rawMaterials'];
|
||||
};
|
||||
|
||||
export default function PurchaseEdit({ purchase, data }: Props) {
|
||||
const { suppliers, rawMaterials } = data;
|
||||
export default function PurchaseEdit({
|
||||
purchase,
|
||||
suppliers,
|
||||
rawMaterials,
|
||||
}: Props) {
|
||||
|
||||
const [supplierId, setSupplierId] = useState(String(purchase.supplier_id));
|
||||
const selectedSupplier =
|
||||
|
||||
@ -50,11 +50,10 @@ type CartLine = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: RestockCreateData;
|
||||
products: RestockCreateData['products'];
|
||||
};
|
||||
|
||||
export default function RestockCreate({ data }: Props) {
|
||||
const { products } = data;
|
||||
export default function RestockCreate({ products }: Props) {
|
||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||
const userId = auth.user?.id;
|
||||
|
||||
|
||||
@ -41,11 +41,10 @@ type CartLine = {
|
||||
|
||||
type Props = {
|
||||
restock: RestockForEdit;
|
||||
data: RestockCreateData;
|
||||
products: RestockCreateData['products'];
|
||||
};
|
||||
|
||||
export default function RestockEdit({ restock, data }: Props) {
|
||||
const { products } = data;
|
||||
export default function RestockEdit({ restock, products }: Props) {
|
||||
|
||||
const [stockType, setStockType] = useState<'good' | 'reject'>(
|
||||
restock.stock_type === 'reject' ? 'reject' : 'good',
|
||||
|
||||
@ -60,13 +60,24 @@ type CartLine = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: TransactionCreateData;
|
||||
products: TransactionCreateData['products'];
|
||||
customers: TransactionCreateData['customers'];
|
||||
employees: TransactionCreateData['employees'];
|
||||
channelOptions: TransactionCreateData['channelOptions'];
|
||||
paymentTypeOptions: TransactionCreateData['paymentTypeOptions'];
|
||||
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
||||
};
|
||||
|
||||
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee'];
|
||||
|
||||
export default function TransactionCreate({ data }: Props) {
|
||||
const { products, customers, employees, channelOptions, paymentTypeOptions, priceTypeOptions } = data;
|
||||
export default function TransactionCreate({
|
||||
products,
|
||||
customers,
|
||||
employees,
|
||||
channelOptions,
|
||||
paymentTypeOptions,
|
||||
priceTypeOptions,
|
||||
}: Props) {
|
||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||
const userId = auth.user?.id;
|
||||
|
||||
|
||||
@ -59,13 +59,25 @@ type CartLine = {
|
||||
|
||||
type Props = {
|
||||
transaction: TransactionForEdit;
|
||||
data: TransactionCreateData;
|
||||
products: TransactionCreateData['products'];
|
||||
customers: TransactionCreateData['customers'];
|
||||
employees: TransactionCreateData['employees'];
|
||||
channelOptions: TransactionCreateData['channelOptions'];
|
||||
paymentTypeOptions: TransactionCreateData['paymentTypeOptions'];
|
||||
priceTypeOptions: TransactionCreateData['priceTypeOptions'];
|
||||
};
|
||||
|
||||
const SELLING_PRICE_TYPES = ['distributor', 'agent', 'sub_agent', 'wholesale', 'retail', 'tiktok', 'shopee'];
|
||||
|
||||
export default function TransactionEdit({ transaction, data }: Props) {
|
||||
const { products, customers, employees, channelOptions, paymentTypeOptions, priceTypeOptions } = data;
|
||||
export default function TransactionEdit({
|
||||
transaction,
|
||||
products,
|
||||
customers,
|
||||
employees,
|
||||
channelOptions,
|
||||
paymentTypeOptions,
|
||||
priceTypeOptions,
|
||||
}: Props) {
|
||||
|
||||
const [stockType, setStockType] = useState<'good' | 'reject'>(
|
||||
transaction.stock_type === 'reject' ? 'reject' : 'good',
|
||||
|
||||
@ -132,4 +132,4 @@
|
||||
});
|
||||
});
|
||||
|
||||
require __DIR__ . '/settings.php';
|
||||
require __DIR__.'/settings.php';
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use App\Notifications\WebPushNotification;
|
||||
use App\Services\Admin\Finance\CashAccountService;
|
||||
use App\Services\Admin\Finance\Cash\CashTransactionService;
|
||||
use App\Services\Admin\Finance\EmployeeAdvanceService;
|
||||
use App\Services\Admin\Finance\ExpenseService;
|
||||
use App\Services\Admin\Finance\PayrollPeriodService;
|
||||
use App\Services\Admin\Finance\Payroll\PayrollPeriodService;
|
||||
use App\Services\Admin\HR\AttendanceService;
|
||||
use App\Services\Admin\HR\LeaveRequestService;
|
||||
use App\Services\Admin\Master\Product\ProductService;
|
||||
@ -283,7 +283,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('CashAccountService deposit sends notification to correct roles', function () {
|
||||
test('CashTransactionService deposit sends notification to correct roles', function () {
|
||||
Notification::fake();
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 1000000]);
|
||||
@ -293,7 +293,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
$this->actingAs($developer);
|
||||
|
||||
$service = new CashAccountService;
|
||||
$service = new CashTransactionService;
|
||||
$service->deposit([
|
||||
'amount' => 500000,
|
||||
'description' => 'Setoran modal',
|
||||
@ -306,7 +306,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
expect($developer->notifications()->first()->title)->toBe('Setoran Kas Toko');
|
||||
});
|
||||
|
||||
test('CashAccountService withdrawal sends notification to correct roles', function () {
|
||||
test('CashTransactionService withdrawal sends notification to correct roles', function () {
|
||||
Notification::fake();
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 1000000]);
|
||||
@ -315,7 +315,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
$this->actingAs($developer);
|
||||
|
||||
$service = new CashAccountService;
|
||||
$service = new CashTransactionService;
|
||||
$service->withdrawal([
|
||||
'amount' => 200000,
|
||||
'description' => 'Penarikan kas',
|
||||
@ -327,7 +327,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
expect($developer->notifications()->first()->title)->toBe('Penarikan Kas Toko');
|
||||
});
|
||||
|
||||
test('CashAccountService updateTransaction sends notification to correct roles', function () {
|
||||
test('CashTransactionService update sends notification to correct roles', function () {
|
||||
Notification::fake();
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 1000000]);
|
||||
@ -336,7 +336,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
$this->actingAs($developer);
|
||||
|
||||
$service = new CashAccountService;
|
||||
$service = new CashTransactionService;
|
||||
$transaction = $service->deposit([
|
||||
'amount' => 500000,
|
||||
'description' => 'Initial deposit',
|
||||
@ -344,7 +344,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$service->updateTransaction($transaction, [
|
||||
$service->update($transaction, [
|
||||
'amount' => 600000,
|
||||
'description' => 'Updated deposit',
|
||||
]);
|
||||
@ -355,7 +355,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
expect($developer->notifications()->where('title', 'Transaksi Kas Diperbarui')->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('CashAccountService deleteTransaction sends notification to correct roles', function () {
|
||||
test('CashTransactionService destroy sends notification to correct roles', function () {
|
||||
Notification::fake();
|
||||
|
||||
$cashAccount = CashAccount::factory()->create(['balance' => 1000000]);
|
||||
@ -364,7 +364,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
$this->actingAs($developer);
|
||||
|
||||
$service = new CashAccountService;
|
||||
$service = new CashTransactionService;
|
||||
$transaction = $service->deposit([
|
||||
'amount' => 500000,
|
||||
'description' => 'To be deleted',
|
||||
@ -372,7 +372,7 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$service->deleteTransaction($transaction);
|
||||
$service->destroy($transaction);
|
||||
|
||||
Notification::assertSentTo($developer, WebPushNotification::class);
|
||||
Notification::assertSentTo($owner, WebPushNotification::class);
|
||||
@ -849,13 +849,13 @@ function notifEmployeeWithRole(string $role = 'Kasir'): array
|
||||
]);
|
||||
});
|
||||
|
||||
test('CashAccountService deposit creates in-app notification record', function () {
|
||||
test('CashTransactionService deposit creates in-app notification record', function () {
|
||||
CashAccount::factory()->create(['balance' => 5000000]);
|
||||
$owner = notifUserWithRole('Owner');
|
||||
|
||||
$this->actingAs($owner);
|
||||
|
||||
$service = new CashAccountService;
|
||||
$service = new CashTransactionService;
|
||||
$service->deposit([
|
||||
'amount' => 250000,
|
||||
'description' => 'Test deposit',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user