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
- 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() {