From 7a1c4efe4ab8776e515f1cbef0542932d3f8d2ed Mon Sep 17 00:00:00 2001
From: Yoga Pangestu
Date: Sun, 28 Jun 2026 16:12:15 +0700
Subject: [PATCH] feat: enhance controller best practices and introduce new
Cutting and Order management features with updated services and validation
handling
---
BEST_PRACTICE.md | 115 ++++++++++++++++++
app/Enums/OwnerVerificationAction.php | 4 +-
app/Enums/PriceType.php | 4 +-
app/Enums/ProductStockQuality.php | 4 +-
.../Admin/Account/AppearanceController.php | 5 +-
.../Admin/Account/ProfileController.php | 14 +--
.../Controllers/Admin/AnalysisController.php | 5 +-
.../Admin/Finance/CashController.php | 36 +++---
.../Finance/EmployeeAdvanceController.php | 27 ++--
.../Admin/Finance/PayrollController.php | 17 +--
.../Admin/Hr/AttendanceController.php | 35 +-----
.../Admin/Hr/EmployeeController.php | 59 ++++-----
.../Admin/Hr/LeaveRequestController.php | 27 ++--
.../{ => Cutting}/CuttingController.php | 23 ++--
.../CuttingDraftItemController.php | 2 +-
.../Manage/{ => Order}/OrderController.php | 32 +++--
.../{ => Order}/OrderDraftItemController.php | 2 +-
.../{ => Purchase}/PurchaseController.php | 2 +-
.../PurchaseDraftItemController.php | 2 +-
.../Manage/{ => Stock}/StockController.php | 2 +-
.../StockRetailController.php} | 14 +--
.../Admin/Manage/StokOpnameController.php | 42 ++-----
.../Admin/Master/CategoryController.php | 2 +-
.../Admin/Master/CustomerController.php | 22 ++--
.../Admin/Master/ProductController.php | 33 ++---
.../Admin/Master/RawMaterialController.php | 30 ++---
.../Admin/NotificationController.php | 54 ++------
.../Admin/System/RoleController.php | 31 +----
.../Admin/System/SettingController.php | 16 +--
.../Concerns/ParsesDataTableQuery.php | 7 --
app/Http/Controllers/HomeController.php | 64 +---------
.../Finance/ApproveEmployeeAdvanceRequest.php | 19 +++
.../Finance/DestroyEmployeeAdvanceRequest.php | 19 +++
.../Finance/PayEmployeeAdvanceRequest.php | 21 ++++
.../Admin/Hr/ApproveLeaveRequestRequest.php | 19 +++
...est.php => StockRetailTransferRequest.php} | 2 +-
.../Manage/StokOpnameAutoSaveRequest.php | 32 +++++
.../Requests/Admin/Master/ProductRequest.php | 4 +-
app/Models/Order.php | 4 +-
app/Models/ProductVariant.php | 2 +-
...EcerHistory.php => StockRetailHistory.php} | 8 +-
.../Finance/EmployeeAdvanceService.php | 18 +++
app/Services/Hr/AttendanceService.php | 43 +++++++
app/Services/Hr/EmployeeService.php | 18 +++
app/Services/Hr/LeaveRequestService.php | 30 +++++
app/Services/Manage/CuttingService.php | 20 +++
app/Services/Manage/OrderService.php | 33 +++--
.../Manage/OwnerVerificationService.php | 14 ++-
...EcerService.php => StockRetailService.php} | 32 ++---
app/Services/Master/CustomerService.php | 5 +
app/Services/Master/ProductService.php | 30 +++--
app/Services/Master/RawMaterialService.php | 13 ++
app/Services/System/AnalysisService.php | 10 +-
app/Services/System/HomepageService.php | 77 ++++++++++++
app/Services/System/NotificationService.php | 69 +++++++++++
app/Services/System/RoleService.php | 26 ++++
.../VerificationChangeFormatter.php | 4 +-
...0_100003_create_product_variants_table.php | 2 +-
...0_create_stock_retail_histories_table.php} | 8 +-
resources/js/components/card/ProductCard.vue | 2 +-
.../js/components/modal/ProductModal.vue | 4 +-
...Modal.vue => StockRetailTransferModal.vue} | 14 +--
resources/js/constants/stock-quality.ts | 2 +-
resources/js/pages/Home.vue | 2 +-
.../admin/manage/orders/form/OrderPosForm.vue | 22 ++--
.../js/pages/admin/master/products/Edit.vue | 2 +-
.../master/products/form/ProductForm.vue | 18 +--
.../products/table/ProductGroupedTable.vue | 16 +--
resources/js/types/order.ts | 2 +-
resources/js/types/product.ts | 8 +-
routes/web.php | 20 +--
71 files changed, 894 insertions(+), 532 deletions(-)
rename app/Http/Controllers/Admin/Manage/{ => Cutting}/CuttingController.php (81%)
rename app/Http/Controllers/Admin/Manage/{ => Cutting}/CuttingDraftItemController.php (96%)
rename app/Http/Controllers/Admin/Manage/{ => Order}/OrderController.php (87%)
rename app/Http/Controllers/Admin/Manage/{ => Order}/OrderDraftItemController.php (96%)
rename app/Http/Controllers/Admin/Manage/{ => Purchase}/PurchaseController.php (98%)
rename app/Http/Controllers/Admin/Manage/{ => Purchase}/PurchaseDraftItemController.php (94%)
rename app/Http/Controllers/Admin/Manage/{ => Stock}/StockController.php (96%)
rename app/Http/Controllers/Admin/Manage/{StockEcerController.php => Stock/StockRetailController.php} (57%)
create mode 100644 app/Http/Requests/Admin/Finance/ApproveEmployeeAdvanceRequest.php
create mode 100644 app/Http/Requests/Admin/Finance/DestroyEmployeeAdvanceRequest.php
create mode 100644 app/Http/Requests/Admin/Finance/PayEmployeeAdvanceRequest.php
create mode 100644 app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php
rename app/Http/Requests/Admin/Manage/{StockEcerTransferRequest.php => StockRetailTransferRequest.php} (94%)
create mode 100644 app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php
rename app/Models/{StockEcerHistory.php => StockRetailHistory.php} (79%)
rename app/Services/Manage/{StockEcerService.php => StockRetailService.php} (81%)
create mode 100644 app/Services/System/HomepageService.php
create mode 100644 app/Services/System/NotificationService.php
rename database/migrations/{2026_06_28_094400_create_stock_ecer_histories_table.php => 2026_06_28_094400_create_stock_retail_histories_table.php} (74%)
rename resources/js/components/modal/{StockEcerTransferModal.vue => StockRetailTransferModal.vue} (93%)
diff --git a/BEST_PRACTICE.md b/BEST_PRACTICE.md
index c8135f1..686865a 100644
--- a/BEST_PRACTICE.md
+++ b/BEST_PRACTICE.md
@@ -169,6 +169,121 @@ ### Model Eloquent
}
```
+### Controller
+
+- **Tugas Controller:**
+ - Controller hanya menangani **request dan response**.
+ - Semua logika bisnis (validasi aturan bisnis, query kompleks, transformasi data, authorization bisnis) **wajib** dipindahkan ke **Service**.
+ - Controller memanggil service, lalu mengembalikan `Inertia::render()`, `redirect()`, atau `response()->json()`.
+
+- **Urutan Method:**
+ 1. `index`
+ 2. `create`
+ 3. `store`
+ 4. `show` (jika ada)
+ 5. `edit`
+ 6. `update`
+ 7. `destroy`
+ 8. Method tambahan (custom action) ditempatkan **di bawah** `destroy`.
+
+- **Form Request:**
+ - **Selalu** gunakan Form Request untuk validasi, sekecil apapun validasinya.
+ - Authorization di Form Request menggunakan `$this->user()`, bukan helper global.
+
+- **Authenticated User:**
+ - **Selalu** ambil user dari `$request->user()` pada method controller.
+ - **Jangan** gunakan `request()->user()`, `auth()->user()`, atau `Auth::user()`.
+
+- **Docblock:**
+ - **Jangan** gunakan PHPDoc/docblock pada controller.
+
+- **Penamaan:**
+ - Variable, method, file, dan elemen sistem lainnya **wajib** menggunakan **Bahasa Inggris**.
+ - Teks yang ditampilkan ke user (flash message, label UI) menggunakan **Bahasa Indonesia**.
+ - Kecuali nama modul yang memang sudah Bahasa Inggris (misal: `customer`, `cutting`, `order`).
+
+- **Flash Message:**
+ - Gunakan helper dari trait `FlashesEntityMessage` untuk pesan standar CRUD:
+ - `flashCreated($entity)` → "{entity} berhasil ditambahkan."
+ - `flashUpdated($entity)` → "{entity} berhasil diperbarui."
+ - `flashDeleted($entity)` → "{entity} berhasil dihapus."
+ - `flashStatusUpdated($entity)` → "Status {entity} berhasil diperbarui."
+ - Gunakan `flashSuccess($message)` **hanya** untuk pesan custom (verifikasi owner, approve/reject, transisi status, dll.).
+
+- **Struktur Folder:**
+ - Jika satu modul memiliki **lebih dari satu controller**, kelompokkan dalam subfolder modul.
+ - **Contoh:**
+ ```
+ app/Http/Controllers/Admin/Manage/
+ Cutting/
+ CuttingController.php
+ CuttingDraftItemController.php
+ Order/
+ OrderController.php
+ OrderDraftItemController.php
+ Purchase/
+ PurchaseController.php
+ PurchaseDraftItemController.php
+ Stock/
+ StockController.php
+ StockRetailController.php
+ StokOpnameController.php ← modul dengan 1 controller tetap di level parent
+ OwnerVerificationController.php
+ ```
+
+- **Contoh:**
+ ```php
+ namespace App\Http\Controllers\Admin\Manage\Order;
+
+ use App\Http\Controllers\Concerns\FlashesEntityMessage;
+ use App\Http\Controllers\Concerns\ParsesDataTableQuery;
+ use App\Http\Controllers\Controller;
+ use App\Http\Requests\Admin\Manage\OrderRequest;
+ use App\Models\Order;
+ use App\Services\Manage\OrderService;
+ use Illuminate\Http\RedirectResponse;
+ use Illuminate\Http\Request;
+ use Inertia\Inertia;
+ use Inertia\Response;
+
+ class OrderController extends Controller
+ {
+ use FlashesEntityMessage, ParsesDataTableQuery;
+
+ public function __construct(
+ private readonly OrderService $orderService,
+ ) {}
+
+ public function index(Request $request): Response
+ {
+ $tableQuery = $this->parseDataTableQuery($request);
+
+ return Inertia::render('admin/manage/orders/Index', [
+ 'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
+ 'filters' => $this->dataTableFilters($tableQuery),
+ ]);
+ }
+
+ public function store(OrderRequest $request): RedirectResponse
+ {
+ $this->orderService->create($request->validated(), $request->user());
+
+ $this->flashCreated('Pesanan');
+
+ return redirect()->route('admin.manage.orders.index');
+ }
+
+ public function destroy(Order $order): RedirectResponse
+ {
+ $this->orderService->delete($order);
+
+ $this->flashDeleted('Pesanan');
+
+ return redirect()->route('admin.manage.orders.index');
+ }
+ }
+ ```
+
## Database & Migrasi
### Struktur File Migrasi
diff --git a/app/Enums/OwnerVerificationAction.php b/app/Enums/OwnerVerificationAction.php
index fea8c8e..c871c11 100644
--- a/app/Enums/OwnerVerificationAction.php
+++ b/app/Enums/OwnerVerificationAction.php
@@ -13,7 +13,7 @@ enum OwnerVerificationAction: string
case DELETE = 'delete';
case TOGGLE_STATUS = 'toggle_status';
case STOCK_VERIFY = 'stock_verify';
- case STOCK_ECER_TRANSFER = 'stock_ecer_transfer';
+ case STOCK_RETAIL_TRANSFER = 'stock_retail_transfer';
public function label(): string
{
@@ -23,7 +23,7 @@ public function label(): string
self::DELETE => 'Hapus',
self::TOGGLE_STATUS => 'Ubah Status',
self::STOCK_VERIFY => 'Verifikasi Stok',
- self::STOCK_ECER_TRANSFER => 'Transfer Stok Ecer',
+ self::STOCK_RETAIL_TRANSFER => 'Transfer Stok Ecer',
};
}
}
diff --git a/app/Enums/PriceType.php b/app/Enums/PriceType.php
index bc23ec6..da9d533 100644
--- a/app/Enums/PriceType.php
+++ b/app/Enums/PriceType.php
@@ -12,7 +12,7 @@ enum PriceType: string
case AGENT = 'agent';
case SUB_AGENT = 'sub_agent';
case GROSIR = 'grosir';
- case ECER = 'ecer';
+ case RETAIL = 'retail';
case TIKTOK = 'tiktok';
case SHOPEE = 'shopee';
case HARGA_MODAL = 'harga_modal';
@@ -24,7 +24,7 @@ public function label(): string
self::AGENT => 'Agen',
self::SUB_AGENT => 'Sub Agen',
self::GROSIR => 'Grosir',
- self::ECER => 'Eceran',
+ self::RETAIL => 'Eceran',
self::TIKTOK => 'TikTok',
self::SHOPEE => 'Shopee',
self::HARGA_MODAL => 'Harga Modal',
diff --git a/app/Enums/ProductStockQuality.php b/app/Enums/ProductStockQuality.php
index cfe5282..d29c1ea 100644
--- a/app/Enums/ProductStockQuality.php
+++ b/app/Enums/ProductStockQuality.php
@@ -10,14 +10,14 @@ enum ProductStockQuality: string
case GOOD = 'good';
case REJECT = 'reject';
- case ECER = 'ecer';
+ case RETAIL = 'retail';
public function label(): string
{
return match ($this) {
self::GOOD => 'Bagus',
self::REJECT => 'Reject',
- self::ECER => 'Eceran',
+ self::RETAIL => 'Eceran',
};
}
}
diff --git a/app/Http/Controllers/Admin/Account/AppearanceController.php b/app/Http/Controllers/Admin/Account/AppearanceController.php
index 0be19ea..68e18b6 100644
--- a/app/Http/Controllers/Admin/Account/AppearanceController.php
+++ b/app/Http/Controllers/Admin/Account/AppearanceController.php
@@ -6,6 +6,7 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Account\UpdateAppearanceRequest;
use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Cookie;
@@ -14,10 +15,10 @@ class AppearanceController extends Controller
{
use FlashesEntityMessage;
- public function edit(): Response
+ public function edit(Request $request): Response
{
return Inertia::render('admin/account/Appearance', [
- 'appearance' => request()->cookie('appearance', 'system'),
+ 'appearance' => $request->cookie('appearance', 'system'),
]);
}
diff --git a/app/Http/Controllers/Admin/Account/ProfileController.php b/app/Http/Controllers/Admin/Account/ProfileController.php
index 4eebb03..12e7506 100644
--- a/app/Http/Controllers/Admin/Account/ProfileController.php
+++ b/app/Http/Controllers/Admin/Account/ProfileController.php
@@ -7,8 +7,9 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Account\UpdateProfileRequest;
use App\Services\Account\ProfileService;
-use App\Support\Media\MediaPresenter;
+use App\Services\Hr\EmployeeService;
use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
@@ -18,18 +19,17 @@ class ProfileController extends Controller
public function __construct(
private readonly ProfileService $profileService,
+ private readonly EmployeeService $employeeService,
) {}
- public function edit(): Response
+ public function edit(Request $request): Response
{
- $user = auth()->user()->load('profile');
+ $editData = $this->employeeService->findForEdit($request->user());
return Inertia::render('admin/account/Profile', [
'genders' => Gender::selectOptions(),
- 'user' => $user,
- 'profilePhoto' => $user->profile
- ? MediaPresenter::first($user->profile, 'profile_photo')
- : null,
+ 'user' => $editData['employee'],
+ 'profilePhoto' => $editData['profilePhoto'],
]);
}
diff --git a/app/Http/Controllers/Admin/AnalysisController.php b/app/Http/Controllers/Admin/AnalysisController.php
index 4c076a5..790552d 100644
--- a/app/Http/Controllers/Admin/AnalysisController.php
+++ b/app/Http/Controllers/Admin/AnalysisController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin;
-use App\Enums\Role;
use App\Http\Controllers\Controller;
use App\Services\System\AnalysisService;
use Carbon\Carbon;
@@ -20,9 +19,7 @@ public function index(Request $request): Response
{
$startDate = $request->query('start_date') ? Carbon::parse($request->query('start_date'))->startOfDay() : null;
$endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
-
$user = $request->user();
- $isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
return Inertia::render('admin/Analysis', [
'filters' => [
@@ -31,7 +28,7 @@ public function index(Request $request): Response
],
'attendance' => $this->analysisService->getAttendance(),
'myAttendance' => $user ? $this->analysisService->getMyAttendance($user, $startDate, $endDate) : null,
- 'isManager' => $isManager,
+ 'isManager' => $this->analysisService->isManager($user),
'cashOverview' => $this->analysisService->getCashOverview(),
'rawMaterialStock' => $this->analysisService->getRawMaterialStock(),
'productStock' => $this->analysisService->getProductStock(),
diff --git a/app/Http/Controllers/Admin/Finance/CashController.php b/app/Http/Controllers/Admin/Finance/CashController.php
index a9806da..6fee9d8 100644
--- a/app/Http/Controllers/Admin/Finance/CashController.php
+++ b/app/Http/Controllers/Admin/Finance/CashController.php
@@ -38,6 +38,24 @@ public function index(Request $request): Response
]);
}
+ public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
+ {
+ $this->cashService->updateDeposit($cashTransaction, $request->validated());
+
+ $this->flashUpdated('Setor kas');
+
+ return redirect()->route('admin.finance.cash.index');
+ }
+
+ public function destroy(CashTransaction $cashTransaction): RedirectResponse
+ {
+ $this->cashService->deleteTransaction($cashTransaction);
+
+ $this->flashDeleted('Setor kas');
+
+ return redirect()->route('admin.finance.cash.index');
+ }
+
public function deposit(DepositRequest $request): RedirectResponse
{
$cashAccount = $this->cashService->getDefaultAccount();
@@ -59,22 +77,4 @@ public function withdraw(WithdrawRequest $request): RedirectResponse
return redirect()->route('admin.finance.cash.index');
}
-
- public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
- {
- $this->cashService->updateDeposit($cashTransaction, $request->validated());
-
- $this->flashSuccess('Setor kas berhasil diperbarui.');
-
- return redirect()->route('admin.finance.cash.index');
- }
-
- public function destroy(CashTransaction $cashTransaction): RedirectResponse
- {
- $this->cashService->deleteTransaction($cashTransaction);
-
- $this->flashSuccess('Setor kas berhasil dihapus.');
-
- return redirect()->route('admin.finance.cash.index');
- }
}
diff --git a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php
index 3b5d709..3ecff39 100644
--- a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php
+++ b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php
@@ -2,11 +2,13 @@
namespace App\Http\Controllers\Admin\Finance;
-use App\Enums\Permission;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
+use App\Http\Requests\Admin\Finance\ApproveEmployeeAdvanceRequest;
+use App\Http\Requests\Admin\Finance\DestroyEmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
+use App\Http\Requests\Admin\Finance\PayEmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\RejectEmployeeAdvanceRequest;
use App\Models\EmployeeAdvance;
use App\Services\Finance\EmployeeAdvanceService;
@@ -27,15 +29,10 @@ public function index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
$status = $request->string('status')->toString();
- $user = $request->user();
+ $pageData = $this->employeeAdvanceService->indexPageData($tableQuery, $request->user(), $status);
return Inertia::render('admin/finance/employee-advances/Index', [
- 'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user, $status),
- 'summary' => $this->employeeAdvanceService->outstandingSummary($user),
- 'authEmployeeId' => $user?->employee?->id,
- 'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
- && ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
- && $user->employee !== null,
+ ...$pageData,
'filters' => $this->dataTableFilters($tableQuery, [
'status' => $status,
]),
@@ -60,18 +57,18 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe
return redirect()->route('admin.finance.employee_advances.index');
}
- public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
+ public function destroy(DestroyEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
- $this->employeeAdvanceService->delete($employeeAdvance, auth()->user());
+ $this->employeeAdvanceService->delete($employeeAdvance, $request->user());
$this->flashDeleted('Kasbon');
return redirect()->route('admin.finance.employee_advances.index');
}
- public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
+ public function approve(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
- $this->employeeAdvanceService->approve($employeeAdvance, auth()->user());
+ $this->employeeAdvanceService->approve($employeeAdvance, $request->user());
$this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.');
@@ -83,7 +80,7 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
$this->employeeAdvanceService->reject(
$employeeAdvance,
$request->validated('reason'),
- auth()->user()
+ $request->user(),
);
$this->flashSuccess('Kasbon berhasil ditolak.');
@@ -91,11 +88,11 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
return redirect()->route('admin.finance.employee_advances.index');
}
- public function pay(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse
+ public function pay(PayEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
{
$payAmount = $request->input('amount') ? (int) $request->input('amount') : null;
- $this->employeeAdvanceService->pay($employeeAdvance, auth()->user(), $payAmount);
+ $this->employeeAdvanceService->pay($employeeAdvance, $request->user(), $payAmount);
$this->flashSuccess('Kasbon berhasil dibayar.');
diff --git a/app/Http/Controllers/Admin/Finance/PayrollController.php b/app/Http/Controllers/Admin/Finance/PayrollController.php
index 279bb99..f0b032f 100644
--- a/app/Http/Controllers/Admin/Finance/PayrollController.php
+++ b/app/Http/Controllers/Admin/Finance/PayrollController.php
@@ -17,7 +17,7 @@
class PayrollController extends Controller
{
- use FlashesEntityMessage,ParsesDataTableQuery;
+ use FlashesEntityMessage, ParsesDataTableQuery;
public function __construct(
private readonly PayrollService $payrollService,
@@ -28,15 +28,16 @@ public function index(Request $request): Response
$tableQuery = $this->parseDataTableQuery($request);
$periodId = $request->integer('period_id') ?: null;
$period = $this->payrollService->resolvePeriod($periodId);
+ $user = $request->user();
return Inertia::render('admin/finance/payroll/Index', [
'periods' => $this->payrollService->listPeriods(),
'currentPeriod' => $period,
'payrolls' => $period
- ? $this->payrollService->paginateForPeriod($period, $tableQuery, auth()->user())
+ ? $this->payrollService->paginateForPeriod($period, $tableQuery, $user)
: null,
'summary' => $period
- ? $this->payrollService->periodSummary($period, auth()->user())
+ ? $this->payrollService->periodSummary($period, $user)
: null,
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
'filters' => $this->dataTableFilters($tableQuery, [
@@ -50,10 +51,10 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr
$this->payrollService->addAdjustment(
$payroll,
$request->validated(),
- auth()->user(),
+ $request->user(),
);
- $this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.');
+ $this->flashCreated('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payroll->payroll_period_id,
@@ -65,10 +66,10 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
$this->payrollService->updateAdjustment(
$payrollAdjustment,
$request->validated(),
- auth()->user(),
+ $request->user(),
);
- $this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
+ $this->flashUpdated('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payrollAdjustment->payroll->payroll_period_id,
@@ -81,7 +82,7 @@ public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): Redirec
$this->payrollService->deleteAdjustment($payrollAdjustment);
- $this->flashSuccess('Penyesuaian gaji berhasil dihapus.');
+ $this->flashDeleted('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payroll->payroll_period_id,
diff --git a/app/Http/Controllers/Admin/Hr/AttendanceController.php b/app/Http/Controllers/Admin/Hr/AttendanceController.php
index 3f31daa..a43d59a 100644
--- a/app/Http/Controllers/Admin/Hr/AttendanceController.php
+++ b/app/Http/Controllers/Admin/Hr/AttendanceController.php
@@ -2,8 +2,6 @@
namespace App\Http\Controllers\Admin\Hr;
-use App\Enums\Permission;
-use App\Enums\Role;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Hr\Attendance\CheckInRequest;
@@ -25,37 +23,14 @@ public function __construct(
public function index(Request $request): Response
{
- $user = $request->user();
- $employee = $user?->employee;
- $isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
-
- $scopedEmployeeId = $isManager ? null : $employee?->id;
- $hasScopedAccess = $isManager || $employee !== null;
-
$start = $request->date('start') ?? now()->startOfMonth();
$end = $request->date('end') ?? now()->endOfMonth();
- return Inertia::render('admin/hr/attendances/Index', [
- 'attendances' => $this->attendanceService->listForCalendar(
- $start,
- $end,
- $scopedEmployeeId,
- $hasScopedAccess,
- ),
- 'todayAttendance' => $employee
- ? $this->attendanceService->todayAttendanceForEmployee($employee)
- : null,
- 'isOnLeave' => $employee
- ? $this->attendanceService->isOnLeaveToday($employee)
- : false,
- 'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false)
- && $employee !== null,
- 'canManageAll' => $isManager,
- 'calendarRange' => [
- 'start' => $start->toDateString(),
- 'end' => $end->toDateString(),
- ],
- ]);
+ return Inertia::render('admin/hr/attendances/Index', $this->attendanceService->indexPageData(
+ $request->user(),
+ $start,
+ $end,
+ ));
}
public function checkIn(CheckInRequest $request): RedirectResponse
diff --git a/app/Http/Controllers/Admin/Hr/EmployeeController.php b/app/Http/Controllers/Admin/Hr/EmployeeController.php
index 0e3fb4b..1e9fc01 100644
--- a/app/Http/Controllers/Admin/Hr/EmployeeController.php
+++ b/app/Http/Controllers/Admin/Hr/EmployeeController.php
@@ -4,7 +4,6 @@
use App\Enums\EmploymentStatus;
use App\Enums\Gender;
-use App\Enums\Role;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
@@ -12,7 +11,6 @@
use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\User;
use App\Services\Hr\EmployeeService;
-use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
@@ -26,11 +24,6 @@ public function __construct(
private readonly EmployeeService $employeeService,
) {}
- private function assignableRoleOptions(): array
- {
- return Role::assignableSelectOptions();
- }
-
public function index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
@@ -53,7 +46,7 @@ public function index(Request $request): Response
'employment_status' => $employmentStatus,
'is_active' => $isActive,
]),
- 'roles' => $this->assignableRoleOptions(),
+ 'roles' => $this->employeeService->assignableRoleOptions(),
'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::selectOptions(),
]);
@@ -64,7 +57,7 @@ public function create(): Response
return Inertia::render('admin/hr/employees/Create', [
'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::selectOptions(),
- 'roles' => $this->assignableRoleOptions(),
+ 'roles' => $this->employeeService->assignableRoleOptions(),
]);
}
@@ -79,16 +72,14 @@ public function store(EmployeeRequest $request): RedirectResponse
public function edit(User $user): Response
{
- $user->load(['profile', 'employee', 'roles']);
+ $editData = $this->employeeService->findForEdit($user);
return Inertia::render('admin/hr/employees/Edit', [
'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::selectOptions(),
- 'roles' => $this->assignableRoleOptions(),
- 'employee' => $user,
- 'profilePhoto' => $user->profile
- ? MediaPresenter::first($user->profile, 'profile_photo')
- : null,
+ 'roles' => $this->employeeService->assignableRoleOptions(),
+ 'employee' => $editData['employee'],
+ 'profilePhoto' => $editData['profilePhoto'],
]);
}
@@ -96,25 +87,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
{
$this->employeeService->update($user, $request->validated());
- $this->flashSuccess('Data pegawai berhasil diperbarui.');
-
- return redirect()->route('admin.hr.employees.index');
- }
-
- public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
- {
- $this->employeeService->toggleStatus($user, $request->validated());
-
- $this->flashSuccess('Status pegawai berhasil diubah.');
-
- return redirect()->route('admin.hr.employees.index');
- }
-
- public function resetPassword(User $user): RedirectResponse
- {
- $this->employeeService->resetPassword($user);
-
- $this->flashSuccess('Kata sandi pegawai berhasil direset.');
+ $this->flashUpdated('Pegawai');
return redirect()->route('admin.hr.employees.index');
}
@@ -127,4 +100,22 @@ public function destroy(User $user): RedirectResponse
return redirect()->route('admin.hr.employees.index');
}
+
+ public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
+ {
+ $this->employeeService->toggleStatus($user, $request->validated());
+
+ $this->flashStatusUpdated('Pegawai');
+
+ return redirect()->route('admin.hr.employees.index');
+ }
+
+ public function resetPassword(User $user): RedirectResponse
+ {
+ $this->employeeService->resetPassword($user);
+
+ $this->flashSuccess('Kata sandi pegawai berhasil direset.');
+
+ return redirect()->route('admin.hr.employees.index');
+ }
}
diff --git a/app/Http/Controllers/Admin/Hr/LeaveRequestController.php b/app/Http/Controllers/Admin/Hr/LeaveRequestController.php
index 8f00c50..71c7abe 100644
--- a/app/Http/Controllers/Admin/Hr/LeaveRequestController.php
+++ b/app/Http/Controllers/Admin/Hr/LeaveRequestController.php
@@ -2,10 +2,10 @@
namespace App\Http\Controllers\Admin\Hr;
-use App\Enums\Permission;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
+use App\Http\Requests\Admin\Hr\ApproveLeaveRequestRequest;
use App\Http\Requests\Admin\Hr\RejectLeaveRequestRequest;
use App\Http\Requests\Admin\Hr\SubmitLeaveRequest;
use App\Models\LeaveRequest;
@@ -26,23 +26,10 @@ public function __construct(
public function index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
- $user = $request->user();
-
- $hasPending = false;
- if ($user?->employee !== null) {
- $hasPending = LeaveRequest::query()
- ->pending()
- ->where('employee_id', $user->employee->id)
- ->exists();
- }
+ $pageData = $this->leaveRequestService->indexPageData($tableQuery, $request->user());
return Inertia::render('admin/hr/leave-requests/Index', [
- 'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery, $user),
- 'authEmployeeId' => $user?->employee?->id,
- 'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value)
- && ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
- && $user->employee !== null,
- 'hasPending' => $hasPending,
+ ...$pageData,
'filters' => $this->dataTableFilters($tableQuery),
]);
}
@@ -67,16 +54,16 @@ public function update(SubmitLeaveRequest $request, LeaveRequest $leaveRequest):
public function destroy(LeaveRequest $leaveRequest): RedirectResponse
{
- $this->leaveRequestService->delete($leaveRequest, auth()->user());
+ $this->leaveRequestService->delete($leaveRequest);
$this->flashDeleted('Pengajuan cuti');
return redirect()->route('admin.hr.leave_requests.index');
}
- public function approve(LeaveRequest $leaveRequest): RedirectResponse
+ public function approve(ApproveLeaveRequestRequest $request, LeaveRequest $leaveRequest): RedirectResponse
{
- $this->leaveRequestService->approve($leaveRequest, auth()->user());
+ $this->leaveRequestService->approve($leaveRequest, $request->user());
$this->flashSuccess('Pengajuan cuti berhasil disetujui.');
@@ -88,7 +75,7 @@ public function reject(RejectLeaveRequestRequest $request, LeaveRequest $leaveRe
$this->leaveRequestService->reject(
$leaveRequest,
$request->validated('reason'),
- auth()->user(),
+ $request->user(),
);
$this->flashSuccess('Pengajuan cuti berhasil ditolak.');
diff --git a/app/Http/Controllers/Admin/Manage/CuttingController.php b/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
similarity index 81%
rename from app/Http/Controllers/Admin/Manage/CuttingController.php
rename to app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
index 43167dd..5cb753a 100644
--- a/app/Http/Controllers/Admin/Manage/CuttingController.php
+++ b/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
@@ -1,6 +1,6 @@
user();
+ $user = $request->user();
return Inertia::render('admin/manage/cuttings/Create', [
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user),
@@ -57,8 +58,10 @@ public function store(CuttingRequest $request): RedirectResponse
public function edit(Cutting $cutting): Response|RedirectResponse
{
- if (! $this->cuttingService->isEditable($cutting->status)) {
- $this->flashError('Proses cutting tidak dapat diubah.');
+ try {
+ $this->cuttingService->ensureEditable($cutting);
+ } catch (ValidationException $exception) {
+ $this->flashError($exception->validator->errors()->first('status'));
return redirect()->route('admin.manage.cuttings.index');
}
@@ -102,15 +105,7 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
$request->validated('result_prices'),
);
- $message = match ($status) {
- CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
- CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
- CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
- CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
- default => 'Status proses cutting berhasil diperbarui.',
- };
-
- $this->flashSuccess($message);
+ $this->flashSuccess($this->cuttingService->transitionStatusMessage($status));
return redirect()->route('admin.manage.cuttings.index');
}
diff --git a/app/Http/Controllers/Admin/Manage/CuttingDraftItemController.php b/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
similarity index 96%
rename from app/Http/Controllers/Admin/Manage/CuttingDraftItemController.php
rename to app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
index e17dad8..a2a1d65 100644
--- a/app/Http/Controllers/Admin/Manage/CuttingDraftItemController.php
+++ b/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
@@ -1,6 +1,6 @@
$this->orderService->findForShow($order),
- ]);
- }
-
public function create(Request $request): Response
{
$user = $request->user();
@@ -73,10 +67,19 @@ public function store(OrderRequest $request): RedirectResponse
return redirect()->route('admin.manage.orders.index', $params);
}
+ public function show(Order $order): Response
+ {
+ return Inertia::render('admin/manage/orders/Show', [
+ 'order' => $this->orderService->findForShow($order),
+ ]);
+ }
+
public function edit(Order $order): Response|RedirectResponse
{
- if (! $this->orderService->isEditable($order->status)) {
- $this->flashError('Pesanan tidak dapat diubah.');
+ try {
+ $this->orderService->ensureEditable($order);
+ } catch (ValidationException $exception) {
+ $this->flashError($exception->validator->errors()->first('status'));
return redirect()->route('admin.manage.orders.index');
}
@@ -116,14 +119,7 @@ public function transitionStatus(OrderStatusTransitionRequest $request, Order $o
$this->orderService->transitionStatus($order, $status);
- $message = match ($status) {
- OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
- OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
- OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
- default => 'Status pesanan berhasil diperbarui.',
- };
-
- $this->flashSuccess($message);
+ $this->flashSuccess($this->orderService->transitionStatusMessage($status));
return redirect()->route('admin.manage.orders.index');
}
diff --git a/app/Http/Controllers/Admin/Manage/OrderDraftItemController.php b/app/Http/Controllers/Admin/Manage/Order/OrderDraftItemController.php
similarity index 96%
rename from app/Http/Controllers/Admin/Manage/OrderDraftItemController.php
rename to app/Http/Controllers/Admin/Manage/Order/OrderDraftItemController.php
index 74581da..7cfd40b 100644
--- a/app/Http/Controllers/Admin/Manage/OrderDraftItemController.php
+++ b/app/Http/Controllers/Admin/Manage/Order/OrderDraftItemController.php
@@ -1,6 +1,6 @@
validated();
- $this->stockEcerService->transfer(
+ $this->stockRetailService->transfer(
$validated['product_variant_id'],
$validated['quantity'],
$request->user(),
diff --git a/app/Http/Controllers/Admin/Manage/StokOpnameController.php b/app/Http/Controllers/Admin/Manage/StokOpnameController.php
index 0e59953..c2f1b1f 100644
--- a/app/Http/Controllers/Admin/Manage/StokOpnameController.php
+++ b/app/Http/Controllers/Admin/Manage/StokOpnameController.php
@@ -5,12 +5,14 @@
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
+use App\Http\Requests\Admin\Manage\StokOpnameAutoSaveRequest;
use App\Http\Requests\Admin\Manage\StokOpnameRejectRequest;
use App\Http\Requests\Admin\Manage\StokOpnameRequest;
use App\Http\Requests\Admin\Manage\StokOpnameSubmitRequest;
use App\Http\Requests\Admin\Manage\StokOpnameVerifyRequest;
use App\Models\StokOpname;
use App\Services\Manage\StokOpnameService;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
@@ -50,36 +52,6 @@ public function store(StokOpnameRequest $request): RedirectResponse
return redirect()->route('admin.manage.stok-opnames.index');
}
- /**
- * Auto-save draft via AJAX (creates or updates silently).
- */
- public function autoSave(Request $request): \Illuminate\Http\JsonResponse
- {
- $user = $request->user();
-
- if (! $user->can(\App\Enums\Permission::STOK_OPNAMES_CREATE->value) &&
- ! $user->can(\App\Enums\Permission::STOK_OPNAMES_UPDATE->value)) {
- return response()->json(['message' => 'Unauthorized'], 403);
- }
-
- $validated = $request->validate([
- 'stok_opname_id' => ['nullable', 'integer', 'exists:stok_opnames,id'],
- 'opname_date' => ['required', 'date'],
- 'notes' => ['nullable', 'string', 'max:1000'],
- 'items' => ['nullable', 'array'],
- 'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
- 'items.*.physical_stock' => ['nullable', 'integer', 'min:0'],
- 'items.*.notes' => ['nullable', 'string', 'max:500'],
- ]);
-
- $stokOpname = $this->stokOpnameService->autoSave($validated, $user);
-
- return response()->json([
- 'stok_opname_id' => $stokOpname->id,
- 'message' => 'Draft tersimpan otomatis.',
- ]);
- }
-
public function edit(StokOpname $stokOpname): Response
{
return Inertia::render('admin/manage/stok-opnames/Edit', [
@@ -106,6 +78,16 @@ public function destroy(StokOpname $stokOpname): RedirectResponse
return redirect()->route('admin.manage.stok-opnames.index');
}
+ public function autoSave(StokOpnameAutoSaveRequest $request): JsonResponse
+ {
+ $stokOpname = $this->stokOpnameService->autoSave($request->validated(), $request->user());
+
+ return response()->json([
+ 'stok_opname_id' => $stokOpname->id,
+ 'message' => 'Draft tersimpan otomatis.',
+ ]);
+ }
+
public function submit(StokOpnameSubmitRequest $request, StokOpname $stokOpname): RedirectResponse
{
$this->stokOpnameService->submit($stokOpname, $request->user());
diff --git a/app/Http/Controllers/Admin/Master/CategoryController.php b/app/Http/Controllers/Admin/Master/CategoryController.php
index 9d9996b..d44ea55 100644
--- a/app/Http/Controllers/Admin/Master/CategoryController.php
+++ b/app/Http/Controllers/Admin/Master/CategoryController.php
@@ -15,7 +15,7 @@
class CategoryController extends Controller
{
- use FlashesEntityMessage,ParsesDataTableQuery;
+ use FlashesEntityMessage, ParsesDataTableQuery;
public function __construct(
private readonly CategoryService $categoryService,
diff --git a/app/Http/Controllers/Admin/Master/CustomerController.php b/app/Http/Controllers/Admin/Master/CustomerController.php
index 6f61452..14677da 100644
--- a/app/Http/Controllers/Admin/Master/CustomerController.php
+++ b/app/Http/Controllers/Admin/Master/CustomerController.php
@@ -41,18 +41,6 @@ public function store(CustomerRequest $request): RedirectResponse
return redirect()->route('admin.master.customers.index');
}
- public function storeApi(CustomerRequest $request): JsonResponse
- {
- $this->customerService->create($request->validated());
-
- $customer = Customer::query()->latest()->first();
-
- return response()->json([
- 'id' => $customer->id,
- 'name' => $customer->name,
- ]);
- }
-
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
{
$this->customerService->update($customer, $request->validated());
@@ -70,4 +58,14 @@ public function destroy(Customer $customer): RedirectResponse
return redirect()->route('admin.master.customers.index');
}
+
+ public function storeApi(CustomerRequest $request): JsonResponse
+ {
+ $customer = $this->customerService->createAndReturn($request->validated());
+
+ return response()->json([
+ 'id' => $customer->id,
+ 'name' => $customer->name,
+ ]);
+ }
}
diff --git a/app/Http/Controllers/Admin/Master/ProductController.php b/app/Http/Controllers/Admin/Master/ProductController.php
index 3d5f9dd..f9eeb80 100644
--- a/app/Http/Controllers/Admin/Master/ProductController.php
+++ b/app/Http/Controllers/Admin/Master/ProductController.php
@@ -8,10 +8,8 @@
use App\Http\Requests\Admin\Master\ProductRequest;
use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\Product;
-use App\Models\ProductVariant;
use App\Services\Master\CategoryService;
use App\Services\Master\ProductService;
-use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
@@ -62,20 +60,9 @@ public function store(ProductRequest $request): RedirectResponse
public function edit(Product $product): Response
{
- $product->load([
- 'categories',
- 'variants' => fn ($query) => $query
- ->with('media')
- ->orderBy('created_at'),
- ]);
-
- $product->variants->each(function (ProductVariant $variant): void {
- $variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
- });
-
return Inertia::render('admin/master/products/Edit', [
'categories' => $this->categoryService->getSelectOptions(),
- 'product' => $product,
+ 'product' => $this->productService->findForEdit($product),
]);
}
@@ -88,15 +75,6 @@ public function update(ProductRequest $request, Product $product): RedirectRespo
return redirect()->route('admin.master.products.index');
}
- public function toggleStatus(ToggleStatusRequest $request, Product $product): RedirectResponse
- {
- $this->productService->toggleStatus($product, $request->validated(), $request->user());
-
- $this->flashSuccess('Perubahan status produk berhasil diajukan dan menunggu verifikasi owner.');
-
- return back();
- }
-
public function destroy(Request $request, Product $product): RedirectResponse
{
$this->productService->delete($product, $request->user());
@@ -105,4 +83,13 @@ public function destroy(Request $request, Product $product): RedirectResponse
return redirect()->route('admin.master.products.index');
}
+
+ public function toggleStatus(ToggleStatusRequest $request, Product $product): RedirectResponse
+ {
+ $this->productService->toggleStatus($product, $request->validated(), $request->user());
+
+ $this->flashSuccess('Perubahan status produk berhasil diajukan dan menunggu verifikasi owner.');
+
+ return back();
+ }
}
diff --git a/app/Http/Controllers/Admin/Master/RawMaterialController.php b/app/Http/Controllers/Admin/Master/RawMaterialController.php
index 4d3c56b..b4121f3 100644
--- a/app/Http/Controllers/Admin/Master/RawMaterialController.php
+++ b/app/Http/Controllers/Admin/Master/RawMaterialController.php
@@ -9,9 +9,7 @@
use App\Http\Requests\Admin\Master\RawMaterialRequest;
use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\RawMaterial;
-use App\Models\RawMaterialPrice;
use App\Services\Master\RawMaterialService;
-use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
@@ -58,17 +56,9 @@ public function store(RawMaterialRequest $request): RedirectResponse
public function edit(RawMaterial $rawMaterial): Response
{
- $rawMaterial->load([
- 'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
- ]);
-
- $rawMaterial->prices->each(function (RawMaterialPrice $price): void {
- $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
- });
-
return Inertia::render('admin/master/raw-materials/Edit', [
'units' => RawMaterialUnit::selectOptions(),
- 'rawMaterial' => $rawMaterial,
+ 'rawMaterial' => $this->rawMaterialService->findForEdit($rawMaterial),
]);
}
@@ -81,15 +71,6 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
return redirect()->route('admin.master.raw_materials.index');
}
- public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
- {
- $this->rawMaterialService->toggleStatus($rawMaterial, $request->validated(), $request->user());
-
- $this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
-
- return back();
- }
-
public function destroy(Request $request, RawMaterial $rawMaterial): RedirectResponse
{
$this->rawMaterialService->delete($rawMaterial, $request->user());
@@ -98,4 +79,13 @@ public function destroy(Request $request, RawMaterial $rawMaterial): RedirectRes
return redirect()->route('admin.master.raw_materials.index');
}
+
+ public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
+ {
+ $this->rawMaterialService->toggleStatus($rawMaterial, $request->validated(), $request->user());
+
+ $this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
+
+ return back();
+ }
}
diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php
index 1c63b9e..b824eca 100644
--- a/app/Http/Controllers/Admin/NotificationController.php
+++ b/app/Http/Controllers/Admin/NotificationController.php
@@ -4,77 +4,47 @@
use App\Http\Controllers\Controller;
use App\Models\Notification;
+use App\Services\System\NotificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
+ public function __construct(
+ private readonly NotificationService $notificationService,
+ ) {}
+
public function index(Request $request): JsonResponse
{
- $user = $request->user();
-
- $notifications = Notification::query()
- ->where('user_id', $user->id)
- ->latest()
- ->limit(20)
- ->get()
- ->map(fn (Notification $n) => [
- 'id' => $n->id,
- 'title' => $n->title,
- 'body' => $n->body,
- 'url' => $n->url,
- 'is_read' => $n->is_read,
- 'created_at' => $n->created_at->toIso8601String(),
- ]);
-
- $unreadCount = Notification::query()
- ->where('user_id', $user->id)
- ->unread()
- ->count();
-
- return response()->json([
- 'notifications' => $notifications,
- 'unread_count' => $unreadCount,
- ]);
+ return response()->json(
+ $this->notificationService->listForUser($request->user()),
+ );
}
public function markAsRead(Request $request, Notification $notification): JsonResponse
{
- if ($notification->user_id !== $request->user()->id) {
- abort(403);
- }
-
- $notification->markAsRead();
+ $this->notificationService->markAsRead($notification, $request->user());
return response()->json(['success' => true]);
}
public function markAllAsRead(Request $request): JsonResponse
{
- Notification::query()
- ->where('user_id', $request->user()->id)
- ->unread()
- ->update(['is_read' => true, 'read_at' => now()]);
+ $this->notificationService->markAllAsRead($request->user());
return response()->json(['success' => true]);
}
public function destroy(Request $request, Notification $notification): JsonResponse
{
- if ($notification->user_id !== $request->user()->id) {
- abort(403);
- }
-
- $notification->delete();
+ $this->notificationService->delete($notification, $request->user());
return response()->json(['success' => true]);
}
public function destroyAll(Request $request): JsonResponse
{
- Notification::query()
- ->where('user_id', $request->user()->id)
- ->delete();
+ $this->notificationService->deleteAll($request->user());
return response()->json(['success' => true]);
}
diff --git a/app/Http/Controllers/Admin/System/RoleController.php b/app/Http/Controllers/Admin/System/RoleController.php
index 699df36..78a0341 100644
--- a/app/Http/Controllers/Admin/System/RoleController.php
+++ b/app/Http/Controllers/Admin/System/RoleController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin\System;
-use App\Enums\Permission as PermissionEnum;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
@@ -34,17 +33,8 @@ public function index(Request $request): Response
public function create(): Response
{
- $permissions = collect(PermissionEnum::cases())
- ->map(fn (PermissionEnum $permission) => [
- 'value' => $permission->value,
- 'label' => $permission->label(),
- 'group' => $permission->group(),
- ])
- ->values()
- ->all();
-
return Inertia::render('admin/system/roles/Create', [
- 'permissions' => $permissions,
+ 'permissions' => $this->roleService->permissionOptions(),
]);
}
@@ -59,24 +49,9 @@ public function store(RoleRequest $request): RedirectResponse
public function edit(Role $role): Response
{
- $role->load('permissions');
-
- $permissions = collect(PermissionEnum::cases())
- ->map(fn (PermissionEnum $permission) => [
- 'value' => $permission->value,
- 'label' => $permission->label(),
- 'group' => $permission->group(),
- ])
- ->values()
- ->all();
-
return Inertia::render('admin/system/roles/Edit', [
- 'role' => [
- 'id' => $role->id,
- 'name' => $role->name,
- 'permissions' => $role->permissions->pluck('name'),
- ],
- 'permissions' => $permissions,
+ ...$this->roleService->findForEdit($role),
+ 'permissions' => $this->roleService->permissionOptions(),
]);
}
diff --git a/app/Http/Controllers/Admin/System/SettingController.php b/app/Http/Controllers/Admin/System/SettingController.php
index 0c22b9c..fb86f86 100644
--- a/app/Http/Controllers/Admin/System/SettingController.php
+++ b/app/Http/Controllers/Admin/System/SettingController.php
@@ -10,13 +10,12 @@
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
use App\Http\Requests\Admin\System\Setting\SystemRequest;
-use App\Models\OwnerVerificationRequest;
+use App\Services\Manage\OwnerVerificationService;
use App\Services\System\Setting\HomepageSettingService;
use App\Services\System\Setting\HrSettingService;
use App\Services\System\Setting\MarketplaceService;
use App\Services\System\Setting\SocialMediaService;
use App\Services\System\Setting\SystemService;
-use App\Settings\MarketplaceSettings;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
@@ -31,6 +30,7 @@ public function __construct(
private readonly MarketplaceService $marketplaceService,
private readonly HrSettingService $hrSettingService,
private readonly HomepageSettingService $homepageSettingService,
+ private readonly OwnerVerificationService $ownerVerificationService,
) {}
public function index(): Response
@@ -41,10 +41,7 @@ public function index(): Response
'marketplace' => $this->marketplaceService->marketplaceData(),
'hr' => $this->hrSettingService->hrData(),
'homepage' => $this->homepageSettingService->homepageData(),
- 'hasPendingMarketplaceVerification' => OwnerVerificationRequest::query()
- ->where('subject_type', MarketplaceSettings::class)
- ->pending()
- ->exists(),
+ 'hasPendingMarketplaceVerification' => $this->ownerVerificationService->hasPendingMarketplaceVerification(),
]);
}
@@ -78,12 +75,7 @@ public function updateMarketplace(MarketplaceRequest $request): RedirectResponse
{
$this->marketplaceService->updateMarketplace($request->validated(), $request->user());
- $hasPending = OwnerVerificationRequest::query()
- ->where('subject_type', MarketplaceSettings::class)
- ->pending()
- ->exists();
-
- if ($hasPending) {
+ if ($this->ownerVerificationService->hasPendingMarketplaceVerification()) {
$this->flashSuccess('Perubahan pengaturan marketplace berhasil diajukan dan menunggu verifikasi owner.');
} else {
$this->flashSuccess('Pengaturan marketplace berhasil disimpan.');
diff --git a/app/Http/Controllers/Concerns/ParsesDataTableQuery.php b/app/Http/Controllers/Concerns/ParsesDataTableQuery.php
index 9bb10b3..895024c 100644
--- a/app/Http/Controllers/Concerns/ParsesDataTableQuery.php
+++ b/app/Http/Controllers/Concerns/ParsesDataTableQuery.php
@@ -6,9 +6,6 @@
trait ParsesDataTableQuery
{
- /**
- * @return array{search: string, sort: string, direction: 'asc'|'desc'}
- */
protected function parseDataTableQuery(Request $request): array
{
return [
@@ -18,10 +15,6 @@ protected function parseDataTableQuery(Request $request): array
];
}
- /**
- * @param array{search: string, sort: string, direction: 'asc'|'desc'} $query
- * @return array{search: string, sort: string, direction: 'asc'|'desc'|null}
- */
protected function dataTableFilters(array $query, array $extra = []): array
{
return array_merge([
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 7a884dbe..9741891 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -2,76 +2,18 @@
namespace App\Http\Controllers;
-use App\Models\Category;
-use App\Models\Product;
-use App\Models\SystemConfiguration;
-use App\Services\Manage\CuttingResultPriceResolver;
-use App\Services\System\Setting\HomepageSettingService;
-use App\Settings\SocialMediaSettings;
-use App\Settings\SystemSettings;
-use App\Support\Media\MediaPresenter;
+use App\Services\System\HomepageService;
use Inertia\Inertia;
use Inertia\Response;
class HomeController extends Controller
{
public function __construct(
- private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
- private readonly HomepageSettingService $homepageSettingService,
+ private readonly HomepageService $homepageService,
) {}
public function index(): Response
{
- $categories = Category::getActiveWithProducts();
-
- $products = Product::getActiveWithVariantsAndCategories();
-
- $allVariantIds = $products
- ->flatMap(fn ($product) => $product->variants->pluck('id'))
- ->all();
-
- $allPricesByVariant = $this->cuttingResultPriceResolver->latestPricesForVariants($allVariantIds);
-
- $products = $products->map(function ($product) use ($allPricesByVariant) {
- $product->variants->each(function ($variant) use ($allPricesByVariant) {
- $variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
- $variantPrices = $allPricesByVariant->get($variant->id, collect());
- $variant->setAttribute(
- 'prices',
- $variantPrices->map(fn ($price) => [
- 'type' => $price->price_type->value,
- 'type_label' => $price->price_type->label(),
- 'price' => (int) $price->price,
- 'price_formatted' => $price->price_formatted,
- ])->values()->all(),
- );
- });
-
- return $product;
- });
-
- $configuration = SystemConfiguration::instance();
- $logo = MediaPresenter::first($configuration, 'logo');
- $logoUrl = $logo['url'] ?? null;
-
- $settings = app(SystemSettings::class);
- $socialSettings = app(SocialMediaSettings::class);
-
- $appName = $settings->app_name ?? 'DST Collection';
-
- return Inertia::render('Home', [
- 'categories' => $categories,
- 'products' => $products,
- 'appName' => $appName,
- 'aboutApp' => $settings->about_app ?? '',
- 'contactEmail' => $settings->email ?? '',
- 'contactPhone' => $settings->phone ?? '',
- 'contactAddress' => $settings->address ?? '',
- 'logoUrl' => $logoUrl,
- 'instagramUrl' => $socialSettings->instagram_url ?? null,
- 'facebookUrl' => $socialSettings->facebook_url ?? null,
- 'tiktokUrl' => $socialSettings->tiktok_url ?? null,
- 'homepage' => $this->homepageSettingService->homepageData(),
- ]);
+ return Inertia::render('Home', $this->homepageService->pageData());
}
}
diff --git a/app/Http/Requests/Admin/Finance/ApproveEmployeeAdvanceRequest.php b/app/Http/Requests/Admin/Finance/ApproveEmployeeAdvanceRequest.php
new file mode 100644
index 0000000..edb030e
--- /dev/null
+++ b/app/Http/Requests/Admin/Finance/ApproveEmployeeAdvanceRequest.php
@@ -0,0 +1,19 @@
+user()?->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) ?? false;
+ }
+
+ public function rules(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Http/Requests/Admin/Finance/DestroyEmployeeAdvanceRequest.php b/app/Http/Requests/Admin/Finance/DestroyEmployeeAdvanceRequest.php
new file mode 100644
index 0000000..f15d270
--- /dev/null
+++ b/app/Http/Requests/Admin/Finance/DestroyEmployeeAdvanceRequest.php
@@ -0,0 +1,19 @@
+user()?->can(Permission::EMPLOYEE_ADVANCES_DELETE->value) ?? false;
+ }
+
+ public function rules(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Http/Requests/Admin/Finance/PayEmployeeAdvanceRequest.php b/app/Http/Requests/Admin/Finance/PayEmployeeAdvanceRequest.php
new file mode 100644
index 0000000..aa35a23
--- /dev/null
+++ b/app/Http/Requests/Admin/Finance/PayEmployeeAdvanceRequest.php
@@ -0,0 +1,21 @@
+user()?->can(Permission::EMPLOYEE_ADVANCES_PAY->value) ?? false;
+ }
+
+ public function rules(): array
+ {
+ return [
+ 'amount' => ['nullable', 'integer', 'min:1'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php b/app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php
new file mode 100644
index 0000000..6414630
--- /dev/null
+++ b/app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php
@@ -0,0 +1,19 @@
+user()?->can(Permission::LEAVE_REQUESTS_VERIFY->value) ?? false;
+ }
+
+ public function rules(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Http/Requests/Admin/Manage/StockEcerTransferRequest.php b/app/Http/Requests/Admin/Manage/StockRetailTransferRequest.php
similarity index 94%
rename from app/Http/Requests/Admin/Manage/StockEcerTransferRequest.php
rename to app/Http/Requests/Admin/Manage/StockRetailTransferRequest.php
index 4ebfae9..9f32ade 100644
--- a/app/Http/Requests/Admin/Manage/StockEcerTransferRequest.php
+++ b/app/Http/Requests/Admin/Manage/StockRetailTransferRequest.php
@@ -6,7 +6,7 @@
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
-class StockEcerTransferRequest extends FormRequest
+class StockRetailTransferRequest extends FormRequest
{
public function authorize(): bool
{
diff --git a/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php b/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php
new file mode 100644
index 0000000..91927fb
--- /dev/null
+++ b/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php
@@ -0,0 +1,32 @@
+user();
+
+ return $user !== null && (
+ $user->can(Permission::STOK_OPNAMES_CREATE->value)
+ || $user->can(Permission::STOK_OPNAMES_UPDATE->value)
+ );
+ }
+
+ public function rules(): array
+ {
+ return [
+ 'stok_opname_id' => ['nullable', 'integer', 'exists:stok_opnames,id'],
+ 'opname_date' => ['required', 'date'],
+ 'notes' => ['nullable', 'string', 'max:1000'],
+ 'items' => ['nullable', 'array'],
+ 'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
+ 'items.*.physical_stock' => ['nullable', 'integer', 'min:0'],
+ 'items.*.notes' => ['nullable', 'string', 'max:500'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/Admin/Master/ProductRequest.php b/app/Http/Requests/Admin/Master/ProductRequest.php
index c7ae080..8840e49 100644
--- a/app/Http/Requests/Admin/Master/ProductRequest.php
+++ b/app/Http/Requests/Admin/Master/ProductRequest.php
@@ -42,7 +42,7 @@ public function rules(): array
],
'variants.*.name' => ['required', 'string', 'max:200'],
'variants.*.stock' => ['required', 'integer', 'min:0'],
- 'variants.*.stock_ecer' => ['required', 'integer', 'min:0'],
+ 'variants.*.stock_retail' => ['required', 'integer', 'min:0'],
...$this->variantImageRules(),
];
}
@@ -60,7 +60,7 @@ public function attributes(): array
'variants' => 'varian',
'variants.*.name' => 'nama varian',
'variants.*.stock' => 'stok',
- 'variants.*.stock_ecer' => 'stok ecer',
+ 'variants.*.stock_retail' => 'stok ecer',
...$this->variantImageAttributes('variants', 'foto varian'),
];
}
diff --git a/app/Models/Order.php b/app/Models/Order.php
index 31451c7..ddcd11a 100644
--- a/app/Models/Order.php
+++ b/app/Models/Order.php
@@ -127,9 +127,9 @@ protected function distributor(Builder $query): void
}
#[Scope]
- protected function ecer(Builder $query): void
+ protected function retail(Builder $query): void
{
- $query->where('price_type', PriceType::ECER);
+ $query->where('price_type', PriceType::RETAIL);
}
#[Scope]
diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php
index 6dcba3a..32b8007 100644
--- a/app/Models/ProductVariant.php
+++ b/app/Models/ProductVariant.php
@@ -26,7 +26,7 @@ protected function casts(): array
return [
'reject_stock' => 'integer',
'stock' => 'integer',
- 'stock_ecer' => 'integer',
+ 'stock_retail' => 'integer',
];
}
diff --git a/app/Models/StockEcerHistory.php b/app/Models/StockRetailHistory.php
similarity index 79%
rename from app/Models/StockEcerHistory.php
rename to app/Models/StockRetailHistory.php
index 073cf3c..536b42f 100644
--- a/app/Models/StockEcerHistory.php
+++ b/app/Models/StockRetailHistory.php
@@ -7,9 +7,9 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Guarded(['id'])]
-class StockEcerHistory extends Model
+class StockRetailHistory extends Model
{
- protected $table = 'stock_ecer_histories';
+ protected $table = 'stock_retail_histories';
public $timestamps = false;
@@ -21,8 +21,8 @@ protected function casts(): array
'quantity' => 'integer',
'stock_after' => 'integer',
'stock_before' => 'integer',
- 'stock_ecer_after' => 'integer',
- 'stock_ecer_before' => 'integer',
+ 'stock_retail_after' => 'integer',
+ 'stock_retail_before' => 'integer',
];
}
diff --git a/app/Services/Finance/EmployeeAdvanceService.php b/app/Services/Finance/EmployeeAdvanceService.php
index 6bc764d..e24ee9f 100644
--- a/app/Services/Finance/EmployeeAdvanceService.php
+++ b/app/Services/Finance/EmployeeAdvanceService.php
@@ -3,6 +3,7 @@
namespace App\Services\Finance;
use App\Enums\EmployeeAdvanceStatus;
+use App\Enums\Permission;
use App\Enums\Role;
use App\Models\EmployeeAdvance;
use App\Models\EmployeeAdvancePayment;
@@ -309,4 +310,21 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
+
+ public function canSubmit(User $user): bool
+ {
+ return $user->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
+ && ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
+ && $user->employee !== null;
+ }
+
+ public function indexPageData(array $tableQuery, User $user, string $status = ''): array
+ {
+ return [
+ 'employeeAdvances' => $this->paginateForIndex($tableQuery, $user, $status),
+ 'summary' => $this->outstandingSummary($user),
+ 'authEmployeeId' => $user->employee?->id,
+ 'canSubmit' => $this->canSubmit($user),
+ ];
+ }
}
diff --git a/app/Services/Hr/AttendanceService.php b/app/Services/Hr/AttendanceService.php
index afc7c0a..f391a5c 100644
--- a/app/Services/Hr/AttendanceService.php
+++ b/app/Services/Hr/AttendanceService.php
@@ -2,6 +2,8 @@
namespace App\Services\Hr;
+use App\Enums\Permission;
+use App\Enums\Role;
use App\Models\Attendance;
use App\Models\Employee;
use App\Models\LeaveRequest;
@@ -175,4 +177,45 @@ public function delete(Attendance $attendance): void
route('admin.hr.attendances.index'),
);
}
+
+ public function resolveAttendanceScope(User $user): array
+ {
+ $employee = $user->employee;
+ $isManager = $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]);
+
+ return [
+ 'scopedEmployeeId' => $isManager ? null : $employee?->id,
+ 'hasScopedAccess' => $isManager || $employee !== null,
+ 'isManager' => $isManager,
+ 'employee' => $employee,
+ 'canCheckIn' => $user->can(Permission::ATTENDANCES_CREATE->value) && $employee !== null,
+ ];
+ }
+
+ public function indexPageData(User $user, CarbonInterface $start, CarbonInterface $end): array
+ {
+ $scope = $this->resolveAttendanceScope($user);
+ $employee = $scope['employee'];
+
+ return [
+ 'attendances' => $this->listForCalendar(
+ $start,
+ $end,
+ $scope['scopedEmployeeId'],
+ $scope['hasScopedAccess'],
+ ),
+ 'todayAttendance' => $employee
+ ? $this->todayAttendanceForEmployee($employee)
+ : null,
+ 'isOnLeave' => $employee
+ ? $this->isOnLeaveToday($employee)
+ : false,
+ 'canCheckIn' => $scope['canCheckIn'],
+ 'canManageAll' => $scope['isManager'],
+ 'calendarRange' => [
+ 'start' => $start->toDateString(),
+ 'end' => $end->toDateString(),
+ ],
+ ];
+ }
}
diff --git a/app/Services/Hr/EmployeeService.php b/app/Services/Hr/EmployeeService.php
index 2589104..3706d47 100644
--- a/app/Services/Hr/EmployeeService.php
+++ b/app/Services/Hr/EmployeeService.php
@@ -7,6 +7,7 @@
use App\Models\User;
use App\Models\UserProfile;
use App\Services\Media\MediaService;
+use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
@@ -58,6 +59,23 @@ public function paginateForIndex(
->withQueryString();
}
+ public function assignableRoleOptions(): array
+ {
+ return Role::assignableSelectOptions();
+ }
+
+ public function findForEdit(User $user): array
+ {
+ $user->load(['profile', 'employee', 'roles']);
+
+ return [
+ 'employee' => $user,
+ 'profilePhoto' => $user->profile
+ ? MediaPresenter::first($user->profile, 'profile_photo')
+ : null,
+ ];
+ }
+
/**
* @param array $validated
*/
diff --git a/app/Services/Hr/LeaveRequestService.php b/app/Services/Hr/LeaveRequestService.php
index 64e5594..16f2ded 100644
--- a/app/Services/Hr/LeaveRequestService.php
+++ b/app/Services/Hr/LeaveRequestService.php
@@ -3,6 +3,7 @@
namespace App\Services\Hr;
use App\Enums\LeaveRequestStatus;
+use App\Enums\Permission;
use App\Models\LeaveRequest;
use App\Models\User;
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
@@ -224,4 +225,33 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
+
+ public function hasPendingForEmployee(User $user): bool
+ {
+ if ($user->employee === null) {
+ return false;
+ }
+
+ return LeaveRequest::query()
+ ->pending()
+ ->where('employee_id', $user->employee->id)
+ ->exists();
+ }
+
+ public function canSubmit(User $user): bool
+ {
+ return $user->can(Permission::LEAVE_REQUESTS_CREATE->value)
+ && ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
+ && $user->employee !== null;
+ }
+
+ public function indexPageData(array $tableQuery, User $user): array
+ {
+ return [
+ 'leaveRequests' => $this->paginateForIndex($tableQuery, $user),
+ 'authEmployeeId' => $user->employee?->id,
+ 'canSubmit' => $this->canSubmit($user),
+ 'hasPending' => $this->hasPendingForEmployee($user),
+ ];
+ }
}
diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php
index 3952229..cd71e0d 100644
--- a/app/Services/Manage/CuttingService.php
+++ b/app/Services/Manage/CuttingService.php
@@ -34,6 +34,26 @@ public function isEditable(CuttingStatus $status): bool
return $status->isEditable();
}
+ public function ensureEditable(Cutting $cutting): void
+ {
+ if (! $this->isEditable($cutting->status)) {
+ throw ValidationException::withMessages([
+ 'status' => 'Proses cutting tidak dapat diubah.',
+ ]);
+ }
+ }
+
+ public function transitionStatusMessage(CuttingStatus $status): string
+ {
+ return match ($status) {
+ CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
+ CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
+ CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
+ CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
+ default => 'Status proses cutting berhasil diperbarui.',
+ };
+ }
+
public function canTransitionTo(CuttingStatus $from, CuttingStatus $to): bool
{
return $from->canTransitionTo($to);
diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php
index eec91af..614b0c2 100644
--- a/app/Services/Manage/OrderService.php
+++ b/app/Services/Manage/OrderService.php
@@ -44,6 +44,25 @@ public function isEditable(OrderStatus $status): bool
return $status->isEditable();
}
+ public function ensureEditable(Order $order): void
+ {
+ if (! $this->isEditable($order->status)) {
+ throw ValidationException::withMessages([
+ 'status' => 'Pesanan tidak dapat diubah.',
+ ]);
+ }
+ }
+
+ public function transitionStatusMessage(OrderStatus $status): string
+ {
+ return match ($status) {
+ OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
+ OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
+ OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
+ default => 'Status pesanan berhasil diperbarui.',
+ };
+ }
+
public function canTransitionTo(OrderStatus $from, OrderStatus $to): bool
{
return $from->canTransitionTo($to);
@@ -76,7 +95,7 @@ public function stockColumn(ProductStockQuality $quality): string
return match ($quality) {
ProductStockQuality::GOOD => 'stock',
ProductStockQuality::REJECT => 'reject_stock',
- ProductStockQuality::ECER => 'stock_ecer',
+ ProductStockQuality::RETAIL => 'stock_retail',
};
}
@@ -331,13 +350,13 @@ public function draftItemsForUser(User $user): array
*/
public function syncDraftItem(array $validated, User $user): array
{
- // Force ecer price type and stock quality for cashier role
+ // Force retail price type and stock quality for cashier role
$priceType = $user->hasRole('cashier')
- ? PriceType::ECER
+ ? PriceType::RETAIL
: PriceType::from($validated['price_type']);
$stockQuality = $user->hasRole('cashier')
- ? ProductStockQuality::ECER
+ ? ProductStockQuality::RETAIL
: ProductStockQuality::from($validated['stock_quality']);
$variant = ProductVariant::query()->findOrFail($validated['product_variant_id']);
@@ -391,9 +410,9 @@ public function removeDraftItem(User $user, ProductVariant $productVariant, Prod
*/
public function resyncDraftPrices(User $user, string $priceTypeValue): array
{
- // Force ecer price type for cashier role
+ // Force retail price type for cashier role
$priceType = $user->hasRole('cashier')
- ? PriceType::ECER
+ ? PriceType::RETAIL
: PriceType::from($priceTypeValue);
$items = $this->draftItemsQuery($user)
@@ -430,7 +449,7 @@ public function create(array $validated, User $user): Order
// Force cashier settings
if ($user->hasRole('cashier')) {
$validated['channel'] = 'store';
- $validated['price_type'] = 'ecer';
+ $validated['price_type'] = 'retail';
$validated['payment_type'] = 'cash';
unset($validated['customer_id']);
}
diff --git a/app/Services/Manage/OwnerVerificationService.php b/app/Services/Manage/OwnerVerificationService.php
index d1766d3..8f99563 100644
--- a/app/Services/Manage/OwnerVerificationService.php
+++ b/app/Services/Manage/OwnerVerificationService.php
@@ -36,9 +36,17 @@ public function __construct(
private readonly PurchaseService $purchaseService,
private readonly PushNotificationService $pushNotificationService,
private readonly MarketplaceService $marketplaceService,
- private readonly StockEcerService $stockEcerService,
+ private readonly StockRetailService $stockRetailService,
) {}
+ public function hasPendingMarketplaceVerification(): bool
+ {
+ return OwnerVerificationRequest::query()
+ ->where('subject_type', MarketplaceSettings::class)
+ ->pending()
+ ->exists();
+ }
+
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
@@ -284,7 +292,7 @@ private function rejectVerificationRequest(OwnerVerificationRequest $request): v
RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request),
Purchase::class => $this->purchaseService->rejectVerificationRequest($request),
MarketplaceSettings::class => null,
- ProductVariant::class => $this->stockEcerService->rejectStockEcerTransfer($request),
+ ProductVariant::class => $this->stockRetailService->rejectStockRetailTransfer($request),
default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.',
]),
@@ -298,7 +306,7 @@ private function applyVerificationRequest(OwnerVerificationRequest $request): vo
RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request),
Purchase::class => $this->purchaseService->applyVerificationRequest($request),
MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request),
- ProductVariant::class => $this->stockEcerService->applyStockEcerTransfer($request),
+ ProductVariant::class => $this->stockRetailService->applyStockRetailTransfer($request),
default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.',
]),
diff --git a/app/Services/Manage/StockEcerService.php b/app/Services/Manage/StockRetailService.php
similarity index 81%
rename from app/Services/Manage/StockEcerService.php
rename to app/Services/Manage/StockRetailService.php
index 81fd5fb..eb0b16c 100644
--- a/app/Services/Manage/StockEcerService.php
+++ b/app/Services/Manage/StockRetailService.php
@@ -6,21 +6,21 @@
use App\Enums\OwnerVerificationStatus;
use App\Models\OwnerVerificationRequest;
use App\Models\ProductVariant;
-use App\Models\StockEcerHistory;
+use App\Models\StockRetailHistory;
use App\Models\User;
use App\Services\System\PushNotificationService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
-class StockEcerService
+class StockRetailService
{
public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
- * Submit a stock ecer transfer request for owner verification.
+ * Submit a stock retail transfer request for owner verification.
*/
public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): OwnerVerificationRequest
{
@@ -41,7 +41,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
$existingPending = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class)
->where('subject_id', $variant->id)
- ->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER)
+ ->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
->where('status', OwnerVerificationStatus::PENDING)
->exists();
@@ -53,7 +53,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
try {
$request = OwnerVerificationRequest::create([
- 'action' => OwnerVerificationAction::STOCK_ECER_TRANSFER,
+ 'action' => OwnerVerificationAction::STOCK_RETAIL_TRANSFER,
'status' => OwnerVerificationStatus::PENDING,
'subject_type' => ProductVariant::class,
'subject_id' => $variant->id,
@@ -61,12 +61,12 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
'payload' => [
'old' => [
'stock' => $variant->stock,
- 'stock_ecer' => $variant->stock_ecer,
+ 'stock_retail' => $variant->stock_retail,
],
'new' => [
'quantity' => $quantity,
'stock' => $variant->stock - $quantity,
- 'stock_ecer' => $variant->stock_ecer + $quantity,
+ 'stock_retail' => $variant->stock_retail + $quantity,
'notes' => $notes,
'variant_name' => $variant->name,
'product_name' => $variant->product?->name ?? '-',
@@ -103,9 +103,9 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
}
/**
- * Apply the stock ecer transfer when owner approves.
+ * Apply the stock retail transfer when owner approves.
*/
- public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
+ public function applyStockRetailTransfer(OwnerVerificationRequest $request): void
{
$payload = $request->payload ?? [];
$newData = $payload['new'] ?? [];
@@ -122,19 +122,19 @@ public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
->firstOrFail();
$stockBefore = $variant->stock;
- $stockEcerBefore = $variant->stock_ecer;
+ $stockRetailBefore = $variant->stock_retail;
$variant->decrement('stock', $quantity);
- $variant->increment('stock_ecer', $quantity);
+ $variant->increment('stock_retail', $quantity);
- StockEcerHistory::create([
+ StockRetailHistory::create([
'product_variant_id' => $variant->id,
'user_id' => $request->submitted_by_id,
'quantity' => $quantity,
'stock_before' => $stockBefore,
- 'stock_ecer_before' => $stockEcerBefore,
+ 'stock_retail_before' => $stockRetailBefore,
'stock_after' => $stockBefore - $quantity,
- 'stock_ecer_after' => $stockEcerBefore + $quantity,
+ 'stock_retail_after' => $stockRetailBefore + $quantity,
'notes' => $newData['notes'] ?? null,
'created_at' => now(),
]);
@@ -142,9 +142,9 @@ public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
}
/**
- * Reject the stock ecer transfer (no-op since nothing changed).
+ * Reject the stock retail transfer (no-op since nothing changed).
*/
- public function rejectStockEcerTransfer(OwnerVerificationRequest $request): void
+ public function rejectStockRetailTransfer(OwnerVerificationRequest $request): void
{
// No-op: nothing was changed yet, so nothing to rollback.
}
diff --git a/app/Services/Master/CustomerService.php b/app/Services/Master/CustomerService.php
index d4391bf..b869123 100644
--- a/app/Services/Master/CustomerService.php
+++ b/app/Services/Master/CustomerService.php
@@ -32,6 +32,11 @@ public function create(array $validated): void
Customer::create($validated);
}
+ public function createAndReturn(array $validated): Customer
+ {
+ return Customer::create($validated);
+ }
+
public function update(Customer $customer, array $validated): void
{
$customer->update($validated);
diff --git a/app/Services/Master/ProductService.php b/app/Services/Master/ProductService.php
index 1842a01..76bc43f 100644
--- a/app/Services/Master/ProductService.php
+++ b/app/Services/Master/ProductService.php
@@ -76,12 +76,12 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
$pendingRequest = $product->pendingOwnerVerificationRequest;
- // Also check for pending stock ecer transfer on variants
+ // Also check for pending stock retail transfer on variants
if ($pendingRequest === null) {
$pendingRequest = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class)
->whereIn('subject_id', $product->variants->pluck('id'))
- ->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER)
+ ->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
->pending()
->latest()
->first();
@@ -100,6 +100,22 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
});
}
+ public function findForEdit(Product $product): Product
+ {
+ $product->load([
+ 'categories',
+ 'variants' => fn ($query) => $query
+ ->with('media')
+ ->orderBy('created_at'),
+ ]);
+
+ $product->variants->each(function (ProductVariant $variant): void {
+ $variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
+ });
+
+ return $product;
+ }
+
/**
* @param array $validated
*/
@@ -119,7 +135,7 @@ public function create(array $validated, User $user): void
$variant = $product->variants()->create([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
- 'stock_ecer' => $variantData['stock_ecer'],
+ 'stock_retail' => $variantData['stock_retail'],
]);
$this->syncVariantImages($variant, $variantData, $index);
@@ -381,7 +397,7 @@ private function applyPayloadToProduct(
$variant->update([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
- 'stock_ecer' => $variantData['stock_ecer'],
+ 'stock_retail' => $variantData['stock_retail'],
]);
if ($verificationRequest !== null) {
@@ -394,7 +410,7 @@ private function applyPayloadToProduct(
$variant = $product->variants()->create([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
- 'stock_ecer' => $variantData['stock_ecer'],
+ 'stock_retail' => $variantData['stock_retail'],
]);
if ($verificationRequest !== null) {
@@ -539,7 +555,7 @@ private function snapshotProduct(Product $product): array
'id' => $variant->id,
'name' => $variant->name,
'stock' => $variant->stock,
- 'stock_ecer' => $variant->stock_ecer,
+ 'stock_retail' => $variant->stock_retail,
])
->all(),
]);
@@ -575,7 +591,7 @@ private function buildPayloadFromValidated(array $validated): array
'id' => $variantData['id'] ?? null,
'name' => $variantData['name'],
'stock' => $variantData['stock'],
- 'stock_ecer' => $variantData['stock_ecer'],
+ 'stock_retail' => $variantData['stock_retail'],
'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
])
->all(),
diff --git a/app/Services/Master/RawMaterialService.php b/app/Services/Master/RawMaterialService.php
index 571c943..2fd7b37 100644
--- a/app/Services/Master/RawMaterialService.php
+++ b/app/Services/Master/RawMaterialService.php
@@ -97,6 +97,19 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
});
}
+ public function findForEdit(RawMaterial $rawMaterial): RawMaterial
+ {
+ $rawMaterial->load([
+ 'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
+ ]);
+
+ $rawMaterial->prices->each(function (RawMaterialPrice $price): void {
+ $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
+ });
+
+ return $rawMaterial;
+ }
+
/**
* @param array $validated
*/
diff --git a/app/Services/System/AnalysisService.php b/app/Services/System/AnalysisService.php
index 9a529ed..00bb366 100644
--- a/app/Services/System/AnalysisService.php
+++ b/app/Services/System/AnalysisService.php
@@ -3,6 +3,7 @@
namespace App\Services\System;
use App\Enums\OrderStatus;
+use App\Enums\Role;
use App\Models\Attendance;
use App\Models\CashAccount;
use App\Models\CashTransaction;
@@ -498,7 +499,7 @@ public function getProductStock(): array
->selectRaw('
SUM(product_variants.stock) as total_stock,
SUM(product_variants.reject_stock) as total_reject,
- SUM(product_variants.stock_ecer) as total_ecer,
+ SUM(product_variants.stock_retail) as total_retail,
COUNT(product_variants.id) as total_variants,
COUNT(DISTINCT products.id) as total_products
')
@@ -516,7 +517,7 @@ public function getProductStock(): array
return [
'total_stock' => (int) ($variants->total_stock ?? 0),
'total_reject' => (int) ($variants->total_reject ?? 0),
- 'total_ecer' => (int) ($variants->total_ecer ?? 0),
+ 'total_retail' => (int) ($variants->total_retail ?? 0),
'total_value' => (int) ($totalValue ?? 0),
'total_products' => (int) ($variants->total_products ?? 0),
'total_variants' => (int) ($variants->total_variants ?? 0),
@@ -577,4 +578,9 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
])
->toArray();
}
+
+ public function isManager(?User $user): bool
+ {
+ return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
+ }
}
diff --git a/app/Services/System/HomepageService.php b/app/Services/System/HomepageService.php
new file mode 100644
index 0000000..345e55e
--- /dev/null
+++ b/app/Services/System/HomepageService.php
@@ -0,0 +1,77 @@
+getProducts();
+
+ $configuration = SystemConfiguration::instance();
+ $logo = MediaPresenter::first($configuration, 'logo');
+ $logoUrl = $logo['url'] ?? null;
+
+ $settings = app(SystemSettings::class);
+ $socialSettings = app(SocialMediaSettings::class);
+
+ return [
+ 'categories' => $categories,
+ 'products' => $products,
+ 'appName' => $settings->app_name ?? 'DST Collection',
+ 'aboutApp' => $settings->about_app ?? '',
+ 'contactEmail' => $settings->email ?? '',
+ 'contactPhone' => $settings->phone ?? '',
+ 'contactAddress' => $settings->address ?? '',
+ 'logoUrl' => $logoUrl,
+ 'instagramUrl' => $socialSettings->instagram_url ?? null,
+ 'facebookUrl' => $socialSettings->facebook_url ?? null,
+ 'tiktokUrl' => $socialSettings->tiktok_url ?? null,
+ 'homepage' => $this->homepageSettingService->homepageData(),
+ ];
+ }
+
+ private function getProducts()
+ {
+ $products = Product::getActiveWithVariantsAndCategories();
+
+ $allVariantIds = $products
+ ->flatMap(fn ($product) => $product->variants->pluck('id'))
+ ->all();
+
+ $allPricesByVariant = $this->cuttingResultPriceResolver->latestPricesForVariants($allVariantIds);
+
+ return $products->map(function ($product) use ($allPricesByVariant) {
+ $product->variants->each(function ($variant) use ($allPricesByVariant) {
+ $variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
+ $variantPrices = $allPricesByVariant->get($variant->id, collect());
+ $variant->setAttribute(
+ 'prices',
+ $variantPrices->map(fn ($price) => [
+ 'type' => $price->price_type->value,
+ 'type_label' => $price->price_type->label(),
+ 'price' => (int) $price->price,
+ 'price_formatted' => $price->price_formatted,
+ ])->values()->all(),
+ );
+ });
+
+ return $product;
+ });
+ }
+}
diff --git a/app/Services/System/NotificationService.php b/app/Services/System/NotificationService.php
new file mode 100644
index 0000000..283472b
--- /dev/null
+++ b/app/Services/System/NotificationService.php
@@ -0,0 +1,69 @@
+where('user_id', $user->id)
+ ->latest()
+ ->limit(20)
+ ->get()
+ ->map(fn (Notification $notification) => [
+ 'id' => $notification->id,
+ 'title' => $notification->title,
+ 'body' => $notification->body,
+ 'url' => $notification->url,
+ 'is_read' => $notification->is_read,
+ 'created_at' => $notification->created_at->toIso8601String(),
+ ]);
+
+ $unreadCount = Notification::query()
+ ->where('user_id', $user->id)
+ ->unread()
+ ->count();
+
+ return [
+ 'notifications' => $notifications,
+ 'unread_count' => $unreadCount,
+ ];
+ }
+
+ public function markAsRead(Notification $notification, User $user): void
+ {
+ if ($notification->user_id !== $user->id) {
+ abort(403);
+ }
+
+ $notification->markAsRead();
+ }
+
+ public function markAllAsRead(User $user): void
+ {
+ Notification::query()
+ ->where('user_id', $user->id)
+ ->unread()
+ ->update(['is_read' => true, 'read_at' => now()]);
+ }
+
+ public function delete(Notification $notification, User $user): void
+ {
+ if ($notification->user_id !== $user->id) {
+ abort(403);
+ }
+
+ $notification->delete();
+ }
+
+ public function deleteAll(User $user): void
+ {
+ Notification::query()
+ ->where('user_id', $user->id)
+ ->delete();
+ }
+}
diff --git a/app/Services/System/RoleService.php b/app/Services/System/RoleService.php
index 9c32b42..c610cb0 100644
--- a/app/Services/System/RoleService.php
+++ b/app/Services/System/RoleService.php
@@ -3,6 +3,7 @@
namespace App\Services\System;
use App\Enums\Role as EnumsRole;
+use App\Enums\Permission as PermissionEnum;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
@@ -31,6 +32,31 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
->withQueryString();
}
+ public function permissionOptions(): array
+ {
+ return collect(PermissionEnum::cases())
+ ->map(fn (PermissionEnum $permission) => [
+ 'value' => $permission->value,
+ 'label' => $permission->label(),
+ 'group' => $permission->group(),
+ ])
+ ->values()
+ ->all();
+ }
+
+ public function findForEdit(Role $role): array
+ {
+ $role->load('permissions');
+
+ return [
+ 'role' => [
+ 'id' => $role->id,
+ 'name' => $role->name,
+ 'permissions' => $role->permissions->pluck('name'),
+ ],
+ ];
+ }
+
public function create(array $validated): void
{
try {
diff --git a/app/Support/OwnerVerification/VerificationChangeFormatter.php b/app/Support/OwnerVerification/VerificationChangeFormatter.php
index 2cf41fc..9b5aa22 100644
--- a/app/Support/OwnerVerification/VerificationChangeFormatter.php
+++ b/app/Support/OwnerVerification/VerificationChangeFormatter.php
@@ -77,7 +77,7 @@ private static function label(string $field): string
'prices' => 'Varian Harga',
'quantity' => 'Jumlah Transfer',
'stock' => 'Stok Bagus',
- 'stock_ecer' => 'Stok Ecer',
+ 'stock_retail' => 'Stok Ecer',
'variant_name' => 'Varian',
'product_name' => 'Produk',
'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform',
@@ -186,7 +186,7 @@ private static function presentValue(string $field, mixed $value): mixed
return implode(', ', $value);
}
- if (in_array($field, ['quantity', 'stock', 'stock_ecer'], true)) {
+ if (in_array($field, ['quantity', 'stock', 'stock_retail'], true)) {
return (int) $value;
}
diff --git a/database/migrations/2026_06_10_100003_create_product_variants_table.php b/database/migrations/2026_06_10_100003_create_product_variants_table.php
index 0e93467..a4e3a3d 100644
--- a/database/migrations/2026_06_10_100003_create_product_variants_table.php
+++ b/database/migrations/2026_06_10_100003_create_product_variants_table.php
@@ -16,7 +16,7 @@ public function up(): void
$table->string('name', 200);
$table->unsignedInteger('stock')->default(0);
$table->unsignedInteger('reject_stock')->default(0);
- $table->unsignedInteger('stock_ecer')->default(0);
+ $table->unsignedInteger('stock_retail')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
diff --git a/database/migrations/2026_06_28_094400_create_stock_ecer_histories_table.php b/database/migrations/2026_06_28_094400_create_stock_retail_histories_table.php
similarity index 74%
rename from database/migrations/2026_06_28_094400_create_stock_ecer_histories_table.php
rename to database/migrations/2026_06_28_094400_create_stock_retail_histories_table.php
index 7c7f859..afb7125 100644
--- a/database/migrations/2026_06_28_094400_create_stock_ecer_histories_table.php
+++ b/database/migrations/2026_06_28_094400_create_stock_retail_histories_table.php
@@ -8,7 +8,7 @@
{
public function up(): void
{
- Schema::create('stock_ecer_histories', function (Blueprint $table) {
+ Schema::create('stock_retail_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete();
@@ -16,9 +16,9 @@ public function up(): void
$table->unsignedInteger('quantity');
$table->unsignedInteger('stock_before');
- $table->unsignedInteger('stock_ecer_before');
+ $table->unsignedInteger('stock_retail_before');
$table->unsignedInteger('stock_after');
- $table->unsignedInteger('stock_ecer_after');
+ $table->unsignedInteger('stock_retail_after');
$table->string('notes')->nullable();
$table->timestamp('created_at')->useCurrent();
@@ -27,6 +27,6 @@ public function up(): void
public function down(): void
{
- Schema::dropIfExists('stock_ecer_histories');
+ Schema::dropIfExists('stock_retail_histories');
}
};
diff --git a/resources/js/components/card/ProductCard.vue b/resources/js/components/card/ProductCard.vue
index 571b806..dbecf0e 100644
--- a/resources/js/components/card/ProductCard.vue
+++ b/resources/js/components/card/ProductCard.vue
@@ -52,7 +52,7 @@ const getProductImage = (product: Product, indexOffset = 0) => {
const getProductPriceRange = (product: Product) => {
const prices = product.variants.flatMap(v =>
- v.prices.filter(p => p.type === 'ecer').map(p => p.price)
+ v.prices.filter(p => p.type === 'retail').map(p => p.price)
);
if (prices.length === 0) {
diff --git a/resources/js/components/modal/ProductModal.vue b/resources/js/components/modal/ProductModal.vue
index fbba607..df1b1eb 100644
--- a/resources/js/components/modal/ProductModal.vue
+++ b/resources/js/components/modal/ProductModal.vue
@@ -14,7 +14,7 @@ const emit = defineEmits<{
}>();
const activeVariant = ref(null);
-const activePriceType = ref('ecer');
+const activePriceType = ref('retail');
const activeImageIndex = ref(0);
const allImages = computed(() => {
@@ -40,7 +40,7 @@ const mainImage = computed(() => {
watch(() => props.product, (newProduct) => {
if (newProduct) {
activeVariant.value = newProduct.variants[0] || null;
- activePriceType.value = 'ecer';
+ activePriceType.value = 'retail';
activeImageIndex.value = 0;
} else {
activeVariant.value = null;
diff --git a/resources/js/components/modal/StockEcerTransferModal.vue b/resources/js/components/modal/StockRetailTransferModal.vue
similarity index 93%
rename from resources/js/components/modal/StockEcerTransferModal.vue
rename to resources/js/components/modal/StockRetailTransferModal.vue
index 937176c..78b2797 100644
--- a/resources/js/components/modal/StockEcerTransferModal.vue
+++ b/resources/js/components/modal/StockRetailTransferModal.vue
@@ -20,7 +20,7 @@ import {
} from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea';
import { apiFetch } from '@/lib/api';
-import { transfer } from '@/routes/admin/manage/stock-ecer';
+import { transfer } from '@/routes/admin/manage/stock-retail';
const open = defineModel('open', { default: false });
@@ -29,7 +29,7 @@ const props = defineProps<{
variantName: string;
productName: string;
stockBagus: number;
- stockEcer: number;
+ stockRetail: number;
}>();
const emit = defineEmits<{
@@ -116,7 +116,7 @@ function formatNumber(value: number): string {
Stok Ecer
- {{ formatNumber(stockEcer) }}
+ {{ formatNumber(stockRetail) }}
@@ -128,9 +128,9 @@ function formatNumber(value: number): string {
- Ecer: {{ variant.stock_ecer ?? 0 }}
+ Ecer: {{ variant.stock_retail ?? 0 }}
Bagus: {{ variant.stock }}
·
@@ -813,7 +813,7 @@ function submit() {
·
{{ item.stock_quality_label ?? (item.stock_quality ===
StockQuality.REJECT
- ? 'Reject' : item.stock_quality === StockQuality.ECER
+ ? 'Reject' : item.stock_quality === StockQuality.RETAIL
? 'Eceran' : 'Bagus') }}
@@ -951,7 +951,7 @@ function submit() {
{{ item.variant_name }}
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject'
- : item.stock_quality === StockQuality.ECER ? 'Eceran' : 'Bagus')
+ : item.stock_quality === StockQuality.RETAIL ? 'Eceran' : 'Bagus')
}}
diff --git a/resources/js/pages/admin/master/products/Edit.vue b/resources/js/pages/admin/master/products/Edit.vue
index f56dcd5..91b0a8c 100644
--- a/resources/js/pages/admin/master/products/Edit.vue
+++ b/resources/js/pages/admin/master/products/Edit.vue
@@ -21,7 +21,7 @@ const initialData = computed(() => ({
id: variant.id,
name: variant.name,
stock: variant.stock,
- stock_ecer: variant.stock_ecer,
+ stock_retail: variant.stock_retail,
images: variant.images ?? [],
})),
}));
diff --git a/resources/js/pages/admin/master/products/form/ProductForm.vue b/resources/js/pages/admin/master/products/form/ProductForm.vue
index cb63b1d..2f7ffe1 100644
--- a/resources/js/pages/admin/master/products/form/ProductForm.vue
+++ b/resources/js/pages/admin/master/products/form/ProductForm.vue
@@ -33,7 +33,7 @@ const props = withDefaults(
id?: number;
name?: string;
stock?: number | string;
- stock_ecer?: number | string;
+ stock_retail?: number | string;
images?: Array<{ id: number; url: string; thumb_url: string }>;
}>;
};
@@ -64,7 +64,7 @@ const {
client_id: createClientId(),
name: '',
stock: '0',
- stock_ecer: '0',
+ stock_retail: '0',
media: createMediaUploadState(),
}),
() => {
@@ -73,7 +73,7 @@ const {
client_id: createClientId(),
name: '',
stock: '0',
- stock_ecer: '0',
+ stock_retail: '0',
media: createMediaUploadState(),
}];
}
@@ -83,7 +83,7 @@ const {
id: variant.id,
name: variant.name ?? '',
stock: variant.stock != null ? String(variant.stock) : '0',
- stock_ecer: variant.stock_ecer != null ? String(variant.stock_ecer) : '0',
+ stock_retail: variant.stock_retail != null ? String(variant.stock_retail) : '0',
media: createMediaUploadState(variant.images ?? []),
}));
},
@@ -126,7 +126,7 @@ function buildFormData(): FormData {
appendToFormData(formData, (formData, index, variant) => {
formData.append(`variants[${index}][name]`, variant.name.trim());
formData.append(`variants[${index}][stock]`, String(Number.parseInt(variant.stock, 10) || 0));
- formData.append(`variants[${index}][stock_ecer]`, String(Number.parseInt(variant.stock_ecer, 10) || 0));
+ formData.append(`variants[${index}][stock_retail]`, String(Number.parseInt(variant.stock_retail, 10) || 0));
appendMediaToFormData(formData, `variants[${index}]`, variant.media);
}, props.method);
@@ -228,12 +228,12 @@ function submit() {
-
+
Stok Ecer
-
-
+
+
diff --git a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
index e189f14..c59411f 100644
--- a/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
+++ b/resources/js/pages/admin/master/products/table/ProductGroupedTable.vue
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { DataTableEmpty } from '@/components/data-table';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
-import StockEcerTransferModal from '@/components/modal/StockEcerTransferModal.vue';
+import StockRetailTransferModal from '@/components/modal/StockRetailTransferModal.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -72,20 +72,20 @@ function rowNumber(index: number): number {
return (props.firstItem ?? 1) + index;
}
-// Stock Ecer Transfer Modal
+// Stock Retail Transfer Modal
const transferModalOpen = ref(false);
const transferVariantId = ref(0);
const transferVariantName = ref('');
const transferProductName = ref('');
const transferStockBagus = ref(0);
-const transferStockEcer = ref(0);
+const transferStockRetail = ref(0);
function openTransferModal(product: ProductListItem, variant: Variant) {
transferVariantId.value = variant.id;
transferVariantName.value = variant.name;
transferProductName.value = product.name;
transferStockBagus.value = variant.stock;
- transferStockEcer.value = variant.stock_ecer;
+ transferStockRetail.value = variant.stock_retail;
transferModalOpen.value = true;
}
@@ -134,7 +134,7 @@ function onTransferSubmitted() {
Total stok ecer
- {{product.variants.reduce((acc, v) => acc + v.stock_ecer, 0)}}
+ {{product.variants.reduce((acc, v) => acc + v.stock_retail, 0)}}
@@ -179,7 +179,7 @@ function onTransferSubmitted() {
{{ formatStock(variant.reject_stock) }}
- {{ formatStock(variant.stock_ecer) }}
+ {{ formatStock(variant.stock_retail) }}
@@ -236,13 +236,13 @@ function onTransferSubmitted() {
-
diff --git a/resources/js/types/order.ts b/resources/js/types/order.ts
index b04d99a..839a81a 100644
--- a/resources/js/types/order.ts
+++ b/resources/js/types/order.ts
@@ -103,7 +103,7 @@ export const STOCK_QUALITY_OPTIONS = [
] as const;
export const STOCK_QUALITY_OPTIONS_CASHIER = [
- { value: 'ecer', label: 'Eceran' },
+ { value: 'retail', label: 'Eceran' },
] as const;
export type OrderEditItem = {
diff --git a/resources/js/types/product.ts b/resources/js/types/product.ts
index 4651717..b20d85f 100644
--- a/resources/js/types/product.ts
+++ b/resources/js/types/product.ts
@@ -3,7 +3,7 @@ export const PRICE_TYPES = [
'agent',
'sub_agent',
'grosir',
- 'ecer',
+ 'retail',
'tiktok',
'shopee',
'harga_modal',
@@ -16,7 +16,7 @@ export const PRICE_TYPE_LABELS: Record = {
agent: 'Agen',
sub_agent: 'Sub Agen',
grosir: 'Grosir',
- ecer: 'Eceran',
+ retail: 'Eceran',
tiktok: 'TikTok',
shopee: 'Shopee',
harga_modal: 'Harga Modal',
@@ -43,7 +43,7 @@ export interface Variant {
name: string;
stock: number;
reject_stock: number;
- stock_ecer: number;
+ stock_retail: number;
prices: Price[];
images: MediaItem[];
}
@@ -95,6 +95,6 @@ export interface ProductVariantFormItem {
id?: number;
name: string;
stock: string | number;
- stock_ecer: string | number;
+ stock_retail: string | number;
media: import('@/types/media').MediaUploadState;
}
diff --git a/routes/web.php b/routes/web.php
index 65b29c0..8a6a43b 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -14,16 +14,16 @@
use App\Http\Controllers\Admin\Hr\AttendanceController;
use App\Http\Controllers\Admin\Hr\EmployeeController;
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
-use App\Http\Controllers\Admin\Manage\CuttingController;
-use App\Http\Controllers\Admin\Manage\CuttingDraftItemController;
-use App\Http\Controllers\Admin\Manage\OrderController;
-use App\Http\Controllers\Admin\Manage\OrderDraftItemController;
+use App\Http\Controllers\Admin\Manage\Cutting\CuttingController;
+use App\Http\Controllers\Admin\Manage\Cutting\CuttingDraftItemController;
+use App\Http\Controllers\Admin\Manage\Order\OrderController;
+use App\Http\Controllers\Admin\Manage\Order\OrderDraftItemController;
use App\Http\Controllers\Admin\Manage\OwnerVerificationController;
-use App\Http\Controllers\Admin\Manage\PurchaseController;
-use App\Http\Controllers\Admin\Manage\PurchaseDraftItemController;
+use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController;
+use App\Http\Controllers\Admin\Manage\Purchase\PurchaseDraftItemController;
use App\Http\Controllers\Admin\Manage\StokOpnameController;
-use App\Http\Controllers\Admin\Manage\StockController;
-use App\Http\Controllers\Admin\Manage\StockEcerController;
+use App\Http\Controllers\Admin\Manage\Stock\StockController;
+use App\Http\Controllers\Admin\Manage\Stock\StockRetailController;
use App\Http\Controllers\Admin\Master\CategoryController;
use App\Http\Controllers\Admin\Master\CustomerController;
use App\Http\Controllers\Admin\Master\ProductController;
@@ -357,10 +357,10 @@
->name('verify');
});
- Route::prefix('stock-ecer')->name('stock-ecer.')
+ Route::prefix('stock-retail')->name('stock-retail.')
->middleware('permission:'.Permission::STOCKS_VIEW->value)
->group(function () {
- Route::post('/transfer', [StockEcerController::class, 'transfer'])->name('transfer');
+ Route::post('/transfer', [StockRetailController::class, 'transfer'])->name('transfer');
});
Route::prefix('stok-opnames')->name('stok-opnames.')