feat: enhance controller best practices and introduce new Cutting and Order management features with updated services and validation handling

This commit is contained in:
Yoga Pangestu 2026-06-28 16:12:15 +07:00
parent 393c36e932
commit 7a1c4efe4a
71 changed files with 894 additions and 532 deletions

View File

@ -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 ## Database & Migrasi
### Struktur File Migrasi ### Struktur File Migrasi

View File

@ -13,7 +13,7 @@ enum OwnerVerificationAction: string
case DELETE = 'delete'; case DELETE = 'delete';
case TOGGLE_STATUS = 'toggle_status'; case TOGGLE_STATUS = 'toggle_status';
case STOCK_VERIFY = 'stock_verify'; case STOCK_VERIFY = 'stock_verify';
case STOCK_ECER_TRANSFER = 'stock_ecer_transfer'; case STOCK_RETAIL_TRANSFER = 'stock_retail_transfer';
public function label(): string public function label(): string
{ {
@ -23,7 +23,7 @@ public function label(): string
self::DELETE => 'Hapus', self::DELETE => 'Hapus',
self::TOGGLE_STATUS => 'Ubah Status', self::TOGGLE_STATUS => 'Ubah Status',
self::STOCK_VERIFY => 'Verifikasi Stok', self::STOCK_VERIFY => 'Verifikasi Stok',
self::STOCK_ECER_TRANSFER => 'Transfer Stok Ecer', self::STOCK_RETAIL_TRANSFER => 'Transfer Stok Ecer',
}; };
} }
} }

View File

@ -12,7 +12,7 @@ enum PriceType: string
case AGENT = 'agent'; case AGENT = 'agent';
case SUB_AGENT = 'sub_agent'; case SUB_AGENT = 'sub_agent';
case GROSIR = 'grosir'; case GROSIR = 'grosir';
case ECER = 'ecer'; case RETAIL = 'retail';
case TIKTOK = 'tiktok'; case TIKTOK = 'tiktok';
case SHOPEE = 'shopee'; case SHOPEE = 'shopee';
case HARGA_MODAL = 'harga_modal'; case HARGA_MODAL = 'harga_modal';
@ -24,7 +24,7 @@ public function label(): string
self::AGENT => 'Agen', self::AGENT => 'Agen',
self::SUB_AGENT => 'Sub Agen', self::SUB_AGENT => 'Sub Agen',
self::GROSIR => 'Grosir', self::GROSIR => 'Grosir',
self::ECER => 'Eceran', self::RETAIL => 'Eceran',
self::TIKTOK => 'TikTok', self::TIKTOK => 'TikTok',
self::SHOPEE => 'Shopee', self::SHOPEE => 'Shopee',
self::HARGA_MODAL => 'Harga Modal', self::HARGA_MODAL => 'Harga Modal',

View File

@ -10,14 +10,14 @@ enum ProductStockQuality: string
case GOOD = 'good'; case GOOD = 'good';
case REJECT = 'reject'; case REJECT = 'reject';
case ECER = 'ecer'; case RETAIL = 'retail';
public function label(): string public function label(): string
{ {
return match ($this) { return match ($this) {
self::GOOD => 'Bagus', self::GOOD => 'Bagus',
self::REJECT => 'Reject', self::REJECT => 'Reject',
self::ECER => 'Eceran', self::RETAIL => 'Eceran',
}; };
} }
} }

View File

@ -6,6 +6,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Account\UpdateAppearanceRequest; use App\Http\Requests\Admin\Account\UpdateAppearanceRequest;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Cookie;
@ -14,10 +15,10 @@ class AppearanceController extends Controller
{ {
use FlashesEntityMessage; use FlashesEntityMessage;
public function edit(): Response public function edit(Request $request): Response
{ {
return Inertia::render('admin/account/Appearance', [ return Inertia::render('admin/account/Appearance', [
'appearance' => request()->cookie('appearance', 'system'), 'appearance' => $request->cookie('appearance', 'system'),
]); ]);
} }

View File

@ -7,8 +7,9 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Account\UpdateProfileRequest; use App\Http\Requests\Admin\Account\UpdateProfileRequest;
use App\Services\Account\ProfileService; use App\Services\Account\ProfileService;
use App\Support\Media\MediaPresenter; use App\Services\Hr\EmployeeService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@ -18,18 +19,17 @@ class ProfileController extends Controller
public function __construct( public function __construct(
private readonly ProfileService $profileService, 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', [ return Inertia::render('admin/account/Profile', [
'genders' => Gender::selectOptions(), 'genders' => Gender::selectOptions(),
'user' => $user, 'user' => $editData['employee'],
'profilePhoto' => $user->profile 'profilePhoto' => $editData['profilePhoto'],
? MediaPresenter::first($user->profile, 'profile_photo')
: null,
]); ]);
} }

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\Role;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\System\AnalysisService; use App\Services\System\AnalysisService;
use Carbon\Carbon; 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; $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; $endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
$user = $request->user(); $user = $request->user();
$isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
return Inertia::render('admin/Analysis', [ return Inertia::render('admin/Analysis', [
'filters' => [ 'filters' => [
@ -31,7 +28,7 @@ public function index(Request $request): Response
], ],
'attendance' => $this->analysisService->getAttendance(), 'attendance' => $this->analysisService->getAttendance(),
'myAttendance' => $user ? $this->analysisService->getMyAttendance($user, $startDate, $endDate) : null, 'myAttendance' => $user ? $this->analysisService->getMyAttendance($user, $startDate, $endDate) : null,
'isManager' => $isManager, 'isManager' => $this->analysisService->isManager($user),
'cashOverview' => $this->analysisService->getCashOverview(), 'cashOverview' => $this->analysisService->getCashOverview(),
'rawMaterialStock' => $this->analysisService->getRawMaterialStock(), 'rawMaterialStock' => $this->analysisService->getRawMaterialStock(),
'productStock' => $this->analysisService->getProductStock(), 'productStock' => $this->analysisService->getProductStock(),

View File

@ -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 public function deposit(DepositRequest $request): RedirectResponse
{ {
$cashAccount = $this->cashService->getDefaultAccount(); $cashAccount = $this->cashService->getDefaultAccount();
@ -59,22 +77,4 @@ public function withdraw(WithdrawRequest $request): RedirectResponse
return redirect()->route('admin.finance.cash.index'); 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');
}
} }

View File

@ -2,11 +2,13 @@
namespace App\Http\Controllers\Admin\Finance; namespace App\Http\Controllers\Admin\Finance;
use App\Enums\Permission;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller; 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\EmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\PayEmployeeAdvanceRequest;
use App\Http\Requests\Admin\Finance\RejectEmployeeAdvanceRequest; use App\Http\Requests\Admin\Finance\RejectEmployeeAdvanceRequest;
use App\Models\EmployeeAdvance; use App\Models\EmployeeAdvance;
use App\Services\Finance\EmployeeAdvanceService; use App\Services\Finance\EmployeeAdvanceService;
@ -27,15 +29,10 @@ public function index(Request $request): Response
{ {
$tableQuery = $this->parseDataTableQuery($request); $tableQuery = $this->parseDataTableQuery($request);
$status = $request->string('status')->toString(); $status = $request->string('status')->toString();
$user = $request->user(); $pageData = $this->employeeAdvanceService->indexPageData($tableQuery, $request->user(), $status);
return Inertia::render('admin/finance/employee-advances/Index', [ return Inertia::render('admin/finance/employee-advances/Index', [
'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user, $status), ...$pageData,
'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,
'filters' => $this->dataTableFilters($tableQuery, [ 'filters' => $this->dataTableFilters($tableQuery, [
'status' => $status, 'status' => $status,
]), ]),
@ -60,18 +57,18 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe
return redirect()->route('admin.finance.employee_advances.index'); 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'); $this->flashDeleted('Kasbon');
return redirect()->route('admin.finance.employee_advances.index'); 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.'); $this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.');
@ -83,7 +80,7 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
$this->employeeAdvanceService->reject( $this->employeeAdvanceService->reject(
$employeeAdvance, $employeeAdvance,
$request->validated('reason'), $request->validated('reason'),
auth()->user() $request->user(),
); );
$this->flashSuccess('Kasbon berhasil ditolak.'); $this->flashSuccess('Kasbon berhasil ditolak.');
@ -91,11 +88,11 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
return redirect()->route('admin.finance.employee_advances.index'); 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; $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.'); $this->flashSuccess('Kasbon berhasil dibayar.');

View File

@ -17,7 +17,7 @@
class PayrollController extends Controller class PayrollController extends Controller
{ {
use FlashesEntityMessage,ParsesDataTableQuery; use FlashesEntityMessage, ParsesDataTableQuery;
public function __construct( public function __construct(
private readonly PayrollService $payrollService, private readonly PayrollService $payrollService,
@ -28,15 +28,16 @@ public function index(Request $request): Response
$tableQuery = $this->parseDataTableQuery($request); $tableQuery = $this->parseDataTableQuery($request);
$periodId = $request->integer('period_id') ?: null; $periodId = $request->integer('period_id') ?: null;
$period = $this->payrollService->resolvePeriod($periodId); $period = $this->payrollService->resolvePeriod($periodId);
$user = $request->user();
return Inertia::render('admin/finance/payroll/Index', [ return Inertia::render('admin/finance/payroll/Index', [
'periods' => $this->payrollService->listPeriods(), 'periods' => $this->payrollService->listPeriods(),
'currentPeriod' => $period, 'currentPeriod' => $period,
'payrolls' => $period 'payrolls' => $period
? $this->payrollService->paginateForPeriod($period, $tableQuery, auth()->user()) ? $this->payrollService->paginateForPeriod($period, $tableQuery, $user)
: null, : null,
'summary' => $period 'summary' => $period
? $this->payrollService->periodSummary($period, auth()->user()) ? $this->payrollService->periodSummary($period, $user)
: null, : null,
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(), 'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
'filters' => $this->dataTableFilters($tableQuery, [ 'filters' => $this->dataTableFilters($tableQuery, [
@ -50,10 +51,10 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr
$this->payrollService->addAdjustment( $this->payrollService->addAdjustment(
$payroll, $payroll,
$request->validated(), $request->validated(),
auth()->user(), $request->user(),
); );
$this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.'); $this->flashCreated('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [ return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payroll->payroll_period_id, 'period_id' => $payroll->payroll_period_id,
@ -65,10 +66,10 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
$this->payrollService->updateAdjustment( $this->payrollService->updateAdjustment(
$payrollAdjustment, $payrollAdjustment,
$request->validated(), $request->validated(),
auth()->user(), $request->user(),
); );
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.'); $this->flashUpdated('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [ return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payrollAdjustment->payroll->payroll_period_id, 'period_id' => $payrollAdjustment->payroll->payroll_period_id,
@ -81,7 +82,7 @@ public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): Redirec
$this->payrollService->deleteAdjustment($payrollAdjustment); $this->payrollService->deleteAdjustment($payrollAdjustment);
$this->flashSuccess('Penyesuaian gaji berhasil dihapus.'); $this->flashDeleted('Penyesuaian gaji');
return redirect()->route('admin.finance.payroll.index', [ return redirect()->route('admin.finance.payroll.index', [
'period_id' => $payroll->payroll_period_id, 'period_id' => $payroll->payroll_period_id,

View File

@ -2,8 +2,6 @@
namespace App\Http\Controllers\Admin\Hr; namespace App\Http\Controllers\Admin\Hr;
use App\Enums\Permission;
use App\Enums\Role;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Hr\Attendance\CheckInRequest; use App\Http\Requests\Admin\Hr\Attendance\CheckInRequest;
@ -25,37 +23,14 @@ public function __construct(
public function index(Request $request): Response 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(); $start = $request->date('start') ?? now()->startOfMonth();
$end = $request->date('end') ?? now()->endOfMonth(); $end = $request->date('end') ?? now()->endOfMonth();
return Inertia::render('admin/hr/attendances/Index', [ return Inertia::render('admin/hr/attendances/Index', $this->attendanceService->indexPageData(
'attendances' => $this->attendanceService->listForCalendar( $request->user(),
$start, $start,
$end, $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(),
],
]);
} }
public function checkIn(CheckInRequest $request): RedirectResponse public function checkIn(CheckInRequest $request): RedirectResponse

View File

@ -4,7 +4,6 @@
use App\Enums\EmploymentStatus; use App\Enums\EmploymentStatus;
use App\Enums\Gender; use App\Enums\Gender;
use App\Enums\Role;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -12,7 +11,6 @@
use App\Http\Requests\Admin\ToggleStatusRequest; use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\User; use App\Models\User;
use App\Services\Hr\EmployeeService; use App\Services\Hr\EmployeeService;
use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
@ -26,11 +24,6 @@ public function __construct(
private readonly EmployeeService $employeeService, private readonly EmployeeService $employeeService,
) {} ) {}
private function assignableRoleOptions(): array
{
return Role::assignableSelectOptions();
}
public function index(Request $request): Response public function index(Request $request): Response
{ {
$tableQuery = $this->parseDataTableQuery($request); $tableQuery = $this->parseDataTableQuery($request);
@ -53,7 +46,7 @@ public function index(Request $request): Response
'employment_status' => $employmentStatus, 'employment_status' => $employmentStatus,
'is_active' => $isActive, 'is_active' => $isActive,
]), ]),
'roles' => $this->assignableRoleOptions(), 'roles' => $this->employeeService->assignableRoleOptions(),
'genders' => Gender::selectOptions(), 'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::selectOptions(), 'employmentStatuses' => EmploymentStatus::selectOptions(),
]); ]);
@ -64,7 +57,7 @@ public function create(): Response
return Inertia::render('admin/hr/employees/Create', [ return Inertia::render('admin/hr/employees/Create', [
'genders' => Gender::selectOptions(), 'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::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 public function edit(User $user): Response
{ {
$user->load(['profile', 'employee', 'roles']); $editData = $this->employeeService->findForEdit($user);
return Inertia::render('admin/hr/employees/Edit', [ return Inertia::render('admin/hr/employees/Edit', [
'genders' => Gender::selectOptions(), 'genders' => Gender::selectOptions(),
'employmentStatuses' => EmploymentStatus::selectOptions(), 'employmentStatuses' => EmploymentStatus::selectOptions(),
'roles' => $this->assignableRoleOptions(), 'roles' => $this->employeeService->assignableRoleOptions(),
'employee' => $user, 'employee' => $editData['employee'],
'profilePhoto' => $user->profile 'profilePhoto' => $editData['profilePhoto'],
? MediaPresenter::first($user->profile, 'profile_photo')
: null,
]); ]);
} }
@ -96,25 +87,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
{ {
$this->employeeService->update($user, $request->validated()); $this->employeeService->update($user, $request->validated());
$this->flashSuccess('Data pegawai berhasil diperbarui.'); $this->flashUpdated('Pegawai');
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.');
return redirect()->route('admin.hr.employees.index'); return redirect()->route('admin.hr.employees.index');
} }
@ -127,4 +100,22 @@ public function destroy(User $user): RedirectResponse
return redirect()->route('admin.hr.employees.index'); 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');
}
} }

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers\Admin\Hr; namespace App\Http\Controllers\Admin\Hr;
use App\Enums\Permission;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller; 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\RejectLeaveRequestRequest;
use App\Http\Requests\Admin\Hr\SubmitLeaveRequest; use App\Http\Requests\Admin\Hr\SubmitLeaveRequest;
use App\Models\LeaveRequest; use App\Models\LeaveRequest;
@ -26,23 +26,10 @@ public function __construct(
public function index(Request $request): Response public function index(Request $request): Response
{ {
$tableQuery = $this->parseDataTableQuery($request); $tableQuery = $this->parseDataTableQuery($request);
$user = $request->user(); $pageData = $this->leaveRequestService->indexPageData($tableQuery, $request->user());
$hasPending = false;
if ($user?->employee !== null) {
$hasPending = LeaveRequest::query()
->pending()
->where('employee_id', $user->employee->id)
->exists();
}
return Inertia::render('admin/hr/leave-requests/Index', [ return Inertia::render('admin/hr/leave-requests/Index', [
'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery, $user), ...$pageData,
'authEmployeeId' => $user?->employee?->id,
'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value)
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
&& $user->employee !== null,
'hasPending' => $hasPending,
'filters' => $this->dataTableFilters($tableQuery), 'filters' => $this->dataTableFilters($tableQuery),
]); ]);
} }
@ -67,16 +54,16 @@ public function update(SubmitLeaveRequest $request, LeaveRequest $leaveRequest):
public function destroy(LeaveRequest $leaveRequest): RedirectResponse public function destroy(LeaveRequest $leaveRequest): RedirectResponse
{ {
$this->leaveRequestService->delete($leaveRequest, auth()->user()); $this->leaveRequestService->delete($leaveRequest);
$this->flashDeleted('Pengajuan cuti'); $this->flashDeleted('Pengajuan cuti');
return redirect()->route('admin.hr.leave_requests.index'); 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.'); $this->flashSuccess('Pengajuan cuti berhasil disetujui.');
@ -88,7 +75,7 @@ public function reject(RejectLeaveRequestRequest $request, LeaveRequest $leaveRe
$this->leaveRequestService->reject( $this->leaveRequestService->reject(
$leaveRequest, $leaveRequest,
$request->validated('reason'), $request->validated('reason'),
auth()->user(), $request->user(),
); );
$this->flashSuccess('Pengajuan cuti berhasil ditolak.'); $this->flashSuccess('Pengajuan cuti berhasil ditolak.');

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Cutting;
use App\Enums\CuttingStatus; use App\Enums\CuttingStatus;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
@ -12,6 +12,7 @@
use App\Services\Manage\CuttingService; use App\Services\Manage\CuttingService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@ -34,9 +35,9 @@ public function index(Request $request): Response
]); ]);
} }
public function create(): Response public function create(Request $request): Response
{ {
$user = request()->user(); $user = $request->user();
return Inertia::render('admin/manage/cuttings/Create', [ return Inertia::render('admin/manage/cuttings/Create', [
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user), 'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user),
@ -57,8 +58,10 @@ public function store(CuttingRequest $request): RedirectResponse
public function edit(Cutting $cutting): Response|RedirectResponse public function edit(Cutting $cutting): Response|RedirectResponse
{ {
if (! $this->cuttingService->isEditable($cutting->status)) { try {
$this->flashError('Proses cutting tidak dapat diubah.'); $this->cuttingService->ensureEditable($cutting);
} catch (ValidationException $exception) {
$this->flashError($exception->validator->errors()->first('status'));
return redirect()->route('admin.manage.cuttings.index'); return redirect()->route('admin.manage.cuttings.index');
} }
@ -102,15 +105,7 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
$request->validated('result_prices'), $request->validated('result_prices'),
); );
$message = match ($status) { $this->flashSuccess($this->cuttingService->transitionStatusMessage($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);
return redirect()->route('admin.manage.cuttings.index'); return redirect()->route('admin.manage.cuttings.index');
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Cutting;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\CuttingDraftMaterialRequest; use App\Http\Requests\Admin\Manage\CuttingDraftMaterialRequest;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Order;
use App\Enums\OrderChannel; use App\Enums\OrderChannel;
use App\Enums\OrderStatus; use App\Enums\OrderStatus;
@ -14,6 +14,7 @@
use App\Services\Manage\OrderService; use App\Services\Manage\OrderService;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@ -35,13 +36,6 @@ public function index(Request $request): Response
]); ]);
} }
public function show(Order $order): Response
{
return Inertia::render('admin/manage/orders/Show', [
'order' => $this->orderService->findForShow($order),
]);
}
public function create(Request $request): Response public function create(Request $request): Response
{ {
$user = $request->user(); $user = $request->user();
@ -73,10 +67,19 @@ public function store(OrderRequest $request): RedirectResponse
return redirect()->route('admin.manage.orders.index', $params); 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 public function edit(Order $order): Response|RedirectResponse
{ {
if (! $this->orderService->isEditable($order->status)) { try {
$this->flashError('Pesanan tidak dapat diubah.'); $this->orderService->ensureEditable($order);
} catch (ValidationException $exception) {
$this->flashError($exception->validator->errors()->first('status'));
return redirect()->route('admin.manage.orders.index'); return redirect()->route('admin.manage.orders.index');
} }
@ -116,14 +119,7 @@ public function transitionStatus(OrderStatusTransitionRequest $request, Order $o
$this->orderService->transitionStatus($order, $status); $this->orderService->transitionStatus($order, $status);
$message = match ($status) { $this->flashSuccess($this->orderService->transitionStatusMessage($status));
OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
default => 'Status pesanan berhasil diperbarui.',
};
$this->flashSuccess($message);
return redirect()->route('admin.manage.orders.index'); return redirect()->route('admin.manage.orders.index');
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Order;
use App\Enums\ProductStockQuality; use App\Enums\ProductStockQuality;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Purchase;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Purchase;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\PurchaseDraftItemRequest; use App\Http\Requests\Admin\Manage\PurchaseDraftItemRequest;

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Stock;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;

View File

@ -1,23 +1,23 @@
<?php <?php
namespace App\Http\Controllers\Admin\Manage; namespace App\Http\Controllers\Admin\Manage\Stock;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\StockEcerTransferRequest; use App\Http\Requests\Admin\Manage\StockRetailTransferRequest;
use App\Services\Manage\StockEcerService; use App\Services\Manage\StockRetailService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
class StockEcerController extends Controller class StockRetailController extends Controller
{ {
public function __construct( public function __construct(
private readonly StockEcerService $stockEcerService, private readonly StockRetailService $stockRetailService,
) {} ) {}
public function transfer(StockEcerTransferRequest $request): JsonResponse public function transfer(StockRetailTransferRequest $request): JsonResponse
{ {
$validated = $request->validated(); $validated = $request->validated();
$this->stockEcerService->transfer( $this->stockRetailService->transfer(
$validated['product_variant_id'], $validated['product_variant_id'],
$validated['quantity'], $validated['quantity'],
$request->user(), $request->user(),

View File

@ -5,12 +5,14 @@
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller; 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\StokOpnameRejectRequest;
use App\Http\Requests\Admin\Manage\StokOpnameRequest; use App\Http\Requests\Admin\Manage\StokOpnameRequest;
use App\Http\Requests\Admin\Manage\StokOpnameSubmitRequest; use App\Http\Requests\Admin\Manage\StokOpnameSubmitRequest;
use App\Http\Requests\Admin\Manage\StokOpnameVerifyRequest; use App\Http\Requests\Admin\Manage\StokOpnameVerifyRequest;
use App\Models\StokOpname; use App\Models\StokOpname;
use App\Services\Manage\StokOpnameService; use App\Services\Manage\StokOpnameService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
@ -50,36 +52,6 @@ public function store(StokOpnameRequest $request): RedirectResponse
return redirect()->route('admin.manage.stok-opnames.index'); 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 public function edit(StokOpname $stokOpname): Response
{ {
return Inertia::render('admin/manage/stok-opnames/Edit', [ 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'); 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 public function submit(StokOpnameSubmitRequest $request, StokOpname $stokOpname): RedirectResponse
{ {
$this->stokOpnameService->submit($stokOpname, $request->user()); $this->stokOpnameService->submit($stokOpname, $request->user());

View File

@ -15,7 +15,7 @@
class CategoryController extends Controller class CategoryController extends Controller
{ {
use FlashesEntityMessage,ParsesDataTableQuery; use FlashesEntityMessage, ParsesDataTableQuery;
public function __construct( public function __construct(
private readonly CategoryService $categoryService, private readonly CategoryService $categoryService,

View File

@ -41,18 +41,6 @@ public function store(CustomerRequest $request): RedirectResponse
return redirect()->route('admin.master.customers.index'); 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 public function update(CustomerRequest $request, Customer $customer): RedirectResponse
{ {
$this->customerService->update($customer, $request->validated()); $this->customerService->update($customer, $request->validated());
@ -70,4 +58,14 @@ public function destroy(Customer $customer): RedirectResponse
return redirect()->route('admin.master.customers.index'); 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,
]);
}
} }

View File

@ -8,10 +8,8 @@
use App\Http\Requests\Admin\Master\ProductRequest; use App\Http\Requests\Admin\Master\ProductRequest;
use App\Http\Requests\Admin\ToggleStatusRequest; use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\Product; use App\Models\Product;
use App\Models\ProductVariant;
use App\Services\Master\CategoryService; use App\Services\Master\CategoryService;
use App\Services\Master\ProductService; use App\Services\Master\ProductService;
use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
@ -62,20 +60,9 @@ public function store(ProductRequest $request): RedirectResponse
public function edit(Product $product): Response 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', [ return Inertia::render('admin/master/products/Edit', [
'categories' => $this->categoryService->getSelectOptions(), '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'); 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 public function destroy(Request $request, Product $product): RedirectResponse
{ {
$this->productService->delete($product, $request->user()); $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'); 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();
}
} }

View File

@ -9,9 +9,7 @@
use App\Http\Requests\Admin\Master\RawMaterialRequest; use App\Http\Requests\Admin\Master\RawMaterialRequest;
use App\Http\Requests\Admin\ToggleStatusRequest; use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\RawMaterial; use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Services\Master\RawMaterialService; use App\Services\Master\RawMaterialService;
use App\Support\Media\MediaPresenter;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
@ -58,17 +56,9 @@ public function store(RawMaterialRequest $request): RedirectResponse
public function edit(RawMaterial $rawMaterial): Response 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', [ return Inertia::render('admin/master/raw-materials/Edit', [
'units' => RawMaterialUnit::selectOptions(), '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'); 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 public function destroy(Request $request, RawMaterial $rawMaterial): RedirectResponse
{ {
$this->rawMaterialService->delete($rawMaterial, $request->user()); $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'); 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();
}
} }

View File

@ -4,77 +4,47 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Notification; use App\Models\Notification;
use App\Services\System\NotificationService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class NotificationController extends Controller class NotificationController extends Controller
{ {
public function __construct(
private readonly NotificationService $notificationService,
) {}
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
$user = $request->user(); return response()->json(
$this->notificationService->listForUser($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,
]);
} }
public function markAsRead(Request $request, Notification $notification): JsonResponse public function markAsRead(Request $request, Notification $notification): JsonResponse
{ {
if ($notification->user_id !== $request->user()->id) { $this->notificationService->markAsRead($notification, $request->user());
abort(403);
}
$notification->markAsRead();
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
public function markAllAsRead(Request $request): JsonResponse public function markAllAsRead(Request $request): JsonResponse
{ {
Notification::query() $this->notificationService->markAllAsRead($request->user());
->where('user_id', $request->user()->id)
->unread()
->update(['is_read' => true, 'read_at' => now()]);
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
public function destroy(Request $request, Notification $notification): JsonResponse public function destroy(Request $request, Notification $notification): JsonResponse
{ {
if ($notification->user_id !== $request->user()->id) { $this->notificationService->delete($notification, $request->user());
abort(403);
}
$notification->delete();
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
public function destroyAll(Request $request): JsonResponse public function destroyAll(Request $request): JsonResponse
{ {
Notification::query() $this->notificationService->deleteAll($request->user());
->where('user_id', $request->user()->id)
->delete();
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin\System; namespace App\Http\Controllers\Admin\System;
use App\Enums\Permission as PermissionEnum;
use App\Http\Controllers\Concerns\FlashesEntityMessage; use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -34,17 +33,8 @@ public function index(Request $request): Response
public function create(): 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', [ 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 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', [ return Inertia::render('admin/system/roles/Edit', [
'role' => [ ...$this->roleService->findForEdit($role),
'id' => $role->id, 'permissions' => $this->roleService->permissionOptions(),
'name' => $role->name,
'permissions' => $role->permissions->pluck('name'),
],
'permissions' => $permissions,
]); ]);
} }

View File

@ -10,13 +10,12 @@
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest; use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest; use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
use App\Http\Requests\Admin\System\Setting\SystemRequest; 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\HomepageSettingService;
use App\Services\System\Setting\HrSettingService; use App\Services\System\Setting\HrSettingService;
use App\Services\System\Setting\MarketplaceService; use App\Services\System\Setting\MarketplaceService;
use App\Services\System\Setting\SocialMediaService; use App\Services\System\Setting\SocialMediaService;
use App\Services\System\Setting\SystemService; use App\Services\System\Setting\SystemService;
use App\Settings\MarketplaceSettings;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@ -31,6 +30,7 @@ public function __construct(
private readonly MarketplaceService $marketplaceService, private readonly MarketplaceService $marketplaceService,
private readonly HrSettingService $hrSettingService, private readonly HrSettingService $hrSettingService,
private readonly HomepageSettingService $homepageSettingService, private readonly HomepageSettingService $homepageSettingService,
private readonly OwnerVerificationService $ownerVerificationService,
) {} ) {}
public function index(): Response public function index(): Response
@ -41,10 +41,7 @@ public function index(): Response
'marketplace' => $this->marketplaceService->marketplaceData(), 'marketplace' => $this->marketplaceService->marketplaceData(),
'hr' => $this->hrSettingService->hrData(), 'hr' => $this->hrSettingService->hrData(),
'homepage' => $this->homepageSettingService->homepageData(), 'homepage' => $this->homepageSettingService->homepageData(),
'hasPendingMarketplaceVerification' => OwnerVerificationRequest::query() 'hasPendingMarketplaceVerification' => $this->ownerVerificationService->hasPendingMarketplaceVerification(),
->where('subject_type', MarketplaceSettings::class)
->pending()
->exists(),
]); ]);
} }
@ -78,12 +75,7 @@ public function updateMarketplace(MarketplaceRequest $request): RedirectResponse
{ {
$this->marketplaceService->updateMarketplace($request->validated(), $request->user()); $this->marketplaceService->updateMarketplace($request->validated(), $request->user());
$hasPending = OwnerVerificationRequest::query() if ($this->ownerVerificationService->hasPendingMarketplaceVerification()) {
->where('subject_type', MarketplaceSettings::class)
->pending()
->exists();
if ($hasPending) {
$this->flashSuccess('Perubahan pengaturan marketplace berhasil diajukan dan menunggu verifikasi owner.'); $this->flashSuccess('Perubahan pengaturan marketplace berhasil diajukan dan menunggu verifikasi owner.');
} else { } else {
$this->flashSuccess('Pengaturan marketplace berhasil disimpan.'); $this->flashSuccess('Pengaturan marketplace berhasil disimpan.');

View File

@ -6,9 +6,6 @@
trait ParsesDataTableQuery trait ParsesDataTableQuery
{ {
/**
* @return array{search: string, sort: string, direction: 'asc'|'desc'}
*/
protected function parseDataTableQuery(Request $request): array protected function parseDataTableQuery(Request $request): array
{ {
return [ 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 protected function dataTableFilters(array $query, array $extra = []): array
{ {
return array_merge([ return array_merge([

View File

@ -2,76 +2,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Category; use App\Services\System\HomepageService;
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 Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
class HomeController extends Controller class HomeController extends Controller
{ {
public function __construct( public function __construct(
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver, private readonly HomepageService $homepageService,
private readonly HomepageSettingService $homepageSettingService,
) {} ) {}
public function index(): Response public function index(): Response
{ {
$categories = Category::getActiveWithProducts(); return Inertia::render('Home', $this->homepageService->pageData());
$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(),
]);
} }
} }

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Admin\Finance;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class ApproveEmployeeAdvanceRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value) ?? false;
}
public function rules(): array
{
return [];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Admin\Finance;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class DestroyEmployeeAdvanceRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::EMPLOYEE_ADVANCES_DELETE->value) ?? false;
}
public function rules(): array
{
return [];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Admin\Finance;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class PayEmployeeAdvanceRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::EMPLOYEE_ADVANCES_PAY->value) ?? false;
}
public function rules(): array
{
return [
'amount' => ['nullable', 'integer', 'min:1'],
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Admin\Hr;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class ApproveLeaveRequestRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::LEAVE_REQUESTS_VERIFY->value) ?? false;
}
public function rules(): array
{
return [];
}
}

View File

@ -6,7 +6,7 @@
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class StockEcerTransferRequest extends FormRequest class StockRetailTransferRequest extends FormRequest
{ {
public function authorize(): bool public function authorize(): bool
{ {

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Admin\Manage;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class StokOpnameAutoSaveRequest extends FormRequest
{
public function authorize(): bool
{
$user = $this->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'],
];
}
}

View File

@ -42,7 +42,7 @@ public function rules(): array
], ],
'variants.*.name' => ['required', 'string', 'max:200'], 'variants.*.name' => ['required', 'string', 'max:200'],
'variants.*.stock' => ['required', 'integer', 'min:0'], 'variants.*.stock' => ['required', 'integer', 'min:0'],
'variants.*.stock_ecer' => ['required', 'integer', 'min:0'], 'variants.*.stock_retail' => ['required', 'integer', 'min:0'],
...$this->variantImageRules(), ...$this->variantImageRules(),
]; ];
} }
@ -60,7 +60,7 @@ public function attributes(): array
'variants' => 'varian', 'variants' => 'varian',
'variants.*.name' => 'nama varian', 'variants.*.name' => 'nama varian',
'variants.*.stock' => 'stok', 'variants.*.stock' => 'stok',
'variants.*.stock_ecer' => 'stok ecer', 'variants.*.stock_retail' => 'stok ecer',
...$this->variantImageAttributes('variants', 'foto varian'), ...$this->variantImageAttributes('variants', 'foto varian'),
]; ];
} }

View File

@ -127,9 +127,9 @@ protected function distributor(Builder $query): void
} }
#[Scope] #[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] #[Scope]

View File

@ -26,7 +26,7 @@ protected function casts(): array
return [ return [
'reject_stock' => 'integer', 'reject_stock' => 'integer',
'stock' => 'integer', 'stock' => 'integer',
'stock_ecer' => 'integer', 'stock_retail' => 'integer',
]; ];
} }

View File

@ -7,9 +7,9 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Guarded(['id'])] #[Guarded(['id'])]
class StockEcerHistory extends Model class StockRetailHistory extends Model
{ {
protected $table = 'stock_ecer_histories'; protected $table = 'stock_retail_histories';
public $timestamps = false; public $timestamps = false;
@ -21,8 +21,8 @@ protected function casts(): array
'quantity' => 'integer', 'quantity' => 'integer',
'stock_after' => 'integer', 'stock_after' => 'integer',
'stock_before' => 'integer', 'stock_before' => 'integer',
'stock_ecer_after' => 'integer', 'stock_retail_after' => 'integer',
'stock_ecer_before' => 'integer', 'stock_retail_before' => 'integer',
]; ];
} }

View File

@ -3,6 +3,7 @@
namespace App\Services\Finance; namespace App\Services\Finance;
use App\Enums\EmployeeAdvanceStatus; use App\Enums\EmployeeAdvanceStatus;
use App\Enums\Permission;
use App\Enums\Role; use App\Enums\Role;
use App\Models\EmployeeAdvance; use App\Models\EmployeeAdvance;
use App\Models\EmployeeAdvancePayment; use App\Models\EmployeeAdvancePayment;
@ -309,4 +310,21 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest(); $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),
];
}
} }

View File

@ -2,6 +2,8 @@
namespace App\Services\Hr; namespace App\Services\Hr;
use App\Enums\Permission;
use App\Enums\Role;
use App\Models\Attendance; use App\Models\Attendance;
use App\Models\Employee; use App\Models\Employee;
use App\Models\LeaveRequest; use App\Models\LeaveRequest;
@ -175,4 +177,45 @@ public function delete(Attendance $attendance): void
route('admin.hr.attendances.index'), 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(),
],
];
}
} }

View File

@ -7,6 +7,7 @@
use App\Models\User; use App\Models\User;
use App\Models\UserProfile; use App\Models\UserProfile;
use App\Services\Media\MediaService; use App\Services\Media\MediaService;
use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -58,6 +59,23 @@ public function paginateForIndex(
->withQueryString(); ->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<string, mixed> $validated * @param array<string, mixed> $validated
*/ */

View File

@ -3,6 +3,7 @@
namespace App\Services\Hr; namespace App\Services\Hr;
use App\Enums\LeaveRequestStatus; use App\Enums\LeaveRequestStatus;
use App\Enums\Permission;
use App\Models\LeaveRequest; use App\Models\LeaveRequest;
use App\Models\User; use App\Models\User;
use App\Services\Concerns\ResolvesAuthenticatedEmployee; use App\Services\Concerns\ResolvesAuthenticatedEmployee;
@ -224,4 +225,33 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest(); $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),
];
}
} }

View File

@ -34,6 +34,26 @@ public function isEditable(CuttingStatus $status): bool
return $status->isEditable(); 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 public function canTransitionTo(CuttingStatus $from, CuttingStatus $to): bool
{ {
return $from->canTransitionTo($to); return $from->canTransitionTo($to);

View File

@ -44,6 +44,25 @@ public function isEditable(OrderStatus $status): bool
return $status->isEditable(); 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 public function canTransitionTo(OrderStatus $from, OrderStatus $to): bool
{ {
return $from->canTransitionTo($to); return $from->canTransitionTo($to);
@ -76,7 +95,7 @@ public function stockColumn(ProductStockQuality $quality): string
return match ($quality) { return match ($quality) {
ProductStockQuality::GOOD => 'stock', ProductStockQuality::GOOD => 'stock',
ProductStockQuality::REJECT => 'reject_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 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 = $user->hasRole('cashier')
? PriceType::ECER ? PriceType::RETAIL
: PriceType::from($validated['price_type']); : PriceType::from($validated['price_type']);
$stockQuality = $user->hasRole('cashier') $stockQuality = $user->hasRole('cashier')
? ProductStockQuality::ECER ? ProductStockQuality::RETAIL
: ProductStockQuality::from($validated['stock_quality']); : ProductStockQuality::from($validated['stock_quality']);
$variant = ProductVariant::query()->findOrFail($validated['product_variant_id']); $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 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 = $user->hasRole('cashier')
? PriceType::ECER ? PriceType::RETAIL
: PriceType::from($priceTypeValue); : PriceType::from($priceTypeValue);
$items = $this->draftItemsQuery($user) $items = $this->draftItemsQuery($user)
@ -430,7 +449,7 @@ public function create(array $validated, User $user): Order
// Force cashier settings // Force cashier settings
if ($user->hasRole('cashier')) { if ($user->hasRole('cashier')) {
$validated['channel'] = 'store'; $validated['channel'] = 'store';
$validated['price_type'] = 'ecer'; $validated['price_type'] = 'retail';
$validated['payment_type'] = 'cash'; $validated['payment_type'] = 'cash';
unset($validated['customer_id']); unset($validated['customer_id']);
} }

View File

@ -36,9 +36,17 @@ public function __construct(
private readonly PurchaseService $purchaseService, private readonly PurchaseService $purchaseService,
private readonly PushNotificationService $pushNotificationService, private readonly PushNotificationService $pushNotificationService,
private readonly MarketplaceService $marketplaceService, 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 * @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), RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request),
Purchase::class => $this->purchaseService->rejectVerificationRequest($request), Purchase::class => $this->purchaseService->rejectVerificationRequest($request),
MarketplaceSettings::class => null, MarketplaceSettings::class => null,
ProductVariant::class => $this->stockEcerService->rejectStockEcerTransfer($request), ProductVariant::class => $this->stockRetailService->rejectStockRetailTransfer($request),
default => throw ValidationException::withMessages([ default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.', 'subject_type' => 'Tipe data verifikasi tidak didukung.',
]), ]),
@ -298,7 +306,7 @@ private function applyVerificationRequest(OwnerVerificationRequest $request): vo
RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request), RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request),
Purchase::class => $this->purchaseService->applyVerificationRequest($request), Purchase::class => $this->purchaseService->applyVerificationRequest($request),
MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request), MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request),
ProductVariant::class => $this->stockEcerService->applyStockEcerTransfer($request), ProductVariant::class => $this->stockRetailService->applyStockRetailTransfer($request),
default => throw ValidationException::withMessages([ default => throw ValidationException::withMessages([
'subject_type' => 'Tipe data verifikasi tidak didukung.', 'subject_type' => 'Tipe data verifikasi tidak didukung.',
]), ]),

View File

@ -6,21 +6,21 @@
use App\Enums\OwnerVerificationStatus; use App\Enums\OwnerVerificationStatus;
use App\Models\OwnerVerificationRequest; use App\Models\OwnerVerificationRequest;
use App\Models\ProductVariant; use App\Models\ProductVariant;
use App\Models\StockEcerHistory; use App\Models\StockRetailHistory;
use App\Models\User; use App\Models\User;
use App\Services\System\PushNotificationService; use App\Services\System\PushNotificationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class StockEcerService class StockRetailService
{ {
public function __construct( public function __construct(
private readonly PushNotificationService $pushNotificationService, 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 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() $existingPending = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class) ->where('subject_type', ProductVariant::class)
->where('subject_id', $variant->id) ->where('subject_id', $variant->id)
->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER) ->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
->where('status', OwnerVerificationStatus::PENDING) ->where('status', OwnerVerificationStatus::PENDING)
->exists(); ->exists();
@ -53,7 +53,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
try { try {
$request = OwnerVerificationRequest::create([ $request = OwnerVerificationRequest::create([
'action' => OwnerVerificationAction::STOCK_ECER_TRANSFER, 'action' => OwnerVerificationAction::STOCK_RETAIL_TRANSFER,
'status' => OwnerVerificationStatus::PENDING, 'status' => OwnerVerificationStatus::PENDING,
'subject_type' => ProductVariant::class, 'subject_type' => ProductVariant::class,
'subject_id' => $variant->id, 'subject_id' => $variant->id,
@ -61,12 +61,12 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
'payload' => [ 'payload' => [
'old' => [ 'old' => [
'stock' => $variant->stock, 'stock' => $variant->stock,
'stock_ecer' => $variant->stock_ecer, 'stock_retail' => $variant->stock_retail,
], ],
'new' => [ 'new' => [
'quantity' => $quantity, 'quantity' => $quantity,
'stock' => $variant->stock - $quantity, 'stock' => $variant->stock - $quantity,
'stock_ecer' => $variant->stock_ecer + $quantity, 'stock_retail' => $variant->stock_retail + $quantity,
'notes' => $notes, 'notes' => $notes,
'variant_name' => $variant->name, 'variant_name' => $variant->name,
'product_name' => $variant->product?->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 ?? []; $payload = $request->payload ?? [];
$newData = $payload['new'] ?? []; $newData = $payload['new'] ?? [];
@ -122,19 +122,19 @@ public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
->firstOrFail(); ->firstOrFail();
$stockBefore = $variant->stock; $stockBefore = $variant->stock;
$stockEcerBefore = $variant->stock_ecer; $stockRetailBefore = $variant->stock_retail;
$variant->decrement('stock', $quantity); $variant->decrement('stock', $quantity);
$variant->increment('stock_ecer', $quantity); $variant->increment('stock_retail', $quantity);
StockEcerHistory::create([ StockRetailHistory::create([
'product_variant_id' => $variant->id, 'product_variant_id' => $variant->id,
'user_id' => $request->submitted_by_id, 'user_id' => $request->submitted_by_id,
'quantity' => $quantity, 'quantity' => $quantity,
'stock_before' => $stockBefore, 'stock_before' => $stockBefore,
'stock_ecer_before' => $stockEcerBefore, 'stock_retail_before' => $stockRetailBefore,
'stock_after' => $stockBefore - $quantity, 'stock_after' => $stockBefore - $quantity,
'stock_ecer_after' => $stockEcerBefore + $quantity, 'stock_retail_after' => $stockRetailBefore + $quantity,
'notes' => $newData['notes'] ?? null, 'notes' => $newData['notes'] ?? null,
'created_at' => now(), '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. // No-op: nothing was changed yet, so nothing to rollback.
} }

View File

@ -32,6 +32,11 @@ public function create(array $validated): void
Customer::create($validated); Customer::create($validated);
} }
public function createAndReturn(array $validated): Customer
{
return Customer::create($validated);
}
public function update(Customer $customer, array $validated): void public function update(Customer $customer, array $validated): void
{ {
$customer->update($validated); $customer->update($validated);

View File

@ -76,12 +76,12 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
$pendingRequest = $product->pendingOwnerVerificationRequest; $pendingRequest = $product->pendingOwnerVerificationRequest;
// Also check for pending stock ecer transfer on variants // Also check for pending stock retail transfer on variants
if ($pendingRequest === null) { if ($pendingRequest === null) {
$pendingRequest = OwnerVerificationRequest::query() $pendingRequest = OwnerVerificationRequest::query()
->where('subject_type', ProductVariant::class) ->where('subject_type', ProductVariant::class)
->whereIn('subject_id', $product->variants->pluck('id')) ->whereIn('subject_id', $product->variants->pluck('id'))
->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER) ->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
->pending() ->pending()
->latest() ->latest()
->first(); ->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<string, mixed> $validated * @param array<string, mixed> $validated
*/ */
@ -119,7 +135,7 @@ public function create(array $validated, User $user): void
$variant = $product->variants()->create([ $variant = $product->variants()->create([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_ecer' => $variantData['stock_ecer'], 'stock_retail' => $variantData['stock_retail'],
]); ]);
$this->syncVariantImages($variant, $variantData, $index); $this->syncVariantImages($variant, $variantData, $index);
@ -381,7 +397,7 @@ private function applyPayloadToProduct(
$variant->update([ $variant->update([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_ecer' => $variantData['stock_ecer'], 'stock_retail' => $variantData['stock_retail'],
]); ]);
if ($verificationRequest !== null) { if ($verificationRequest !== null) {
@ -394,7 +410,7 @@ private function applyPayloadToProduct(
$variant = $product->variants()->create([ $variant = $product->variants()->create([
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_ecer' => $variantData['stock_ecer'], 'stock_retail' => $variantData['stock_retail'],
]); ]);
if ($verificationRequest !== null) { if ($verificationRequest !== null) {
@ -539,7 +555,7 @@ private function snapshotProduct(Product $product): array
'id' => $variant->id, 'id' => $variant->id,
'name' => $variant->name, 'name' => $variant->name,
'stock' => $variant->stock, 'stock' => $variant->stock,
'stock_ecer' => $variant->stock_ecer, 'stock_retail' => $variant->stock_retail,
]) ])
->all(), ->all(),
]); ]);
@ -575,7 +591,7 @@ private function buildPayloadFromValidated(array $validated): array
'id' => $variantData['id'] ?? null, 'id' => $variantData['id'] ?? null,
'name' => $variantData['name'], 'name' => $variantData['name'],
'stock' => $variantData['stock'], 'stock' => $variantData['stock'],
'stock_ecer' => $variantData['stock_ecer'], 'stock_retail' => $variantData['stock_retail'],
'remove_media_ids' => $variantData['remove_media_ids'] ?? [], 'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
]) ])
->all(), ->all(),

View File

@ -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<string, mixed> $validated * @param array<string, mixed> $validated
*/ */

View File

@ -3,6 +3,7 @@
namespace App\Services\System; namespace App\Services\System;
use App\Enums\OrderStatus; use App\Enums\OrderStatus;
use App\Enums\Role;
use App\Models\Attendance; use App\Models\Attendance;
use App\Models\CashAccount; use App\Models\CashAccount;
use App\Models\CashTransaction; use App\Models\CashTransaction;
@ -498,7 +499,7 @@ public function getProductStock(): array
->selectRaw(' ->selectRaw('
SUM(product_variants.stock) as total_stock, SUM(product_variants.stock) as total_stock,
SUM(product_variants.reject_stock) as total_reject, 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(product_variants.id) as total_variants,
COUNT(DISTINCT products.id) as total_products COUNT(DISTINCT products.id) as total_products
') ')
@ -516,7 +517,7 @@ public function getProductStock(): array
return [ return [
'total_stock' => (int) ($variants->total_stock ?? 0), 'total_stock' => (int) ($variants->total_stock ?? 0),
'total_reject' => (int) ($variants->total_reject ?? 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_value' => (int) ($totalValue ?? 0),
'total_products' => (int) ($variants->total_products ?? 0), 'total_products' => (int) ($variants->total_products ?? 0),
'total_variants' => (int) ($variants->total_variants ?? 0), 'total_variants' => (int) ($variants->total_variants ?? 0),
@ -577,4 +578,9 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
]) ])
->toArray(); ->toArray();
} }
public function isManager(?User $user): bool
{
return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
}
} }

View File

@ -0,0 +1,77 @@
<?php
namespace App\Services\System;
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;
class HomepageService
{
public function __construct(
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
private readonly HomepageSettingService $homepageSettingService,
) {}
public function pageData(): array
{
$categories = Category::getActiveWithProducts();
$products = $this->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;
});
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Services\System;
use App\Models\Notification;
use App\Models\User;
class NotificationService
{
public function listForUser(User $user): array
{
$notifications = Notification::query()
->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();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\System; namespace App\Services\System;
use App\Enums\Role as EnumsRole; use App\Enums\Role as EnumsRole;
use App\Enums\Permission as PermissionEnum;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -31,6 +32,31 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
->withQueryString(); ->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 public function create(array $validated): void
{ {
try { try {

View File

@ -77,7 +77,7 @@ private static function label(string $field): string
'prices' => 'Varian Harga', 'prices' => 'Varian Harga',
'quantity' => 'Jumlah Transfer', 'quantity' => 'Jumlah Transfer',
'stock' => 'Stok Bagus', 'stock' => 'Stok Bagus',
'stock_ecer' => 'Stok Ecer', 'stock_retail' => 'Stok Ecer',
'variant_name' => 'Varian', 'variant_name' => 'Varian',
'product_name' => 'Produk', 'product_name' => 'Produk',
'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform', 'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform',
@ -186,7 +186,7 @@ private static function presentValue(string $field, mixed $value): mixed
return implode(', ', $value); return implode(', ', $value);
} }
if (in_array($field, ['quantity', 'stock', 'stock_ecer'], true)) { if (in_array($field, ['quantity', 'stock', 'stock_retail'], true)) {
return (int) $value; return (int) $value;
} }

View File

@ -16,7 +16,7 @@ public function up(): void
$table->string('name', 200); $table->string('name', 200);
$table->unsignedInteger('stock')->default(0); $table->unsignedInteger('stock')->default(0);
$table->unsignedInteger('reject_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('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();

View File

@ -8,7 +8,7 @@
{ {
public function up(): void public function up(): void
{ {
Schema::create('stock_ecer_histories', function (Blueprint $table) { Schema::create('stock_retail_histories', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete(); $table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete();
@ -16,9 +16,9 @@ public function up(): void
$table->unsignedInteger('quantity'); $table->unsignedInteger('quantity');
$table->unsignedInteger('stock_before'); $table->unsignedInteger('stock_before');
$table->unsignedInteger('stock_ecer_before'); $table->unsignedInteger('stock_retail_before');
$table->unsignedInteger('stock_after'); $table->unsignedInteger('stock_after');
$table->unsignedInteger('stock_ecer_after'); $table->unsignedInteger('stock_retail_after');
$table->string('notes')->nullable(); $table->string('notes')->nullable();
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
@ -27,6 +27,6 @@ public function up(): void
public function down(): void public function down(): void
{ {
Schema::dropIfExists('stock_ecer_histories'); Schema::dropIfExists('stock_retail_histories');
} }
}; };

View File

@ -52,7 +52,7 @@ const getProductImage = (product: Product, indexOffset = 0) => {
const getProductPriceRange = (product: Product) => { const getProductPriceRange = (product: Product) => {
const prices = product.variants.flatMap(v => 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) { if (prices.length === 0) {

View File

@ -14,7 +14,7 @@ const emit = defineEmits<{
}>(); }>();
const activeVariant = ref<Variant | null>(null); const activeVariant = ref<Variant | null>(null);
const activePriceType = ref<string>('ecer'); const activePriceType = ref<string>('retail');
const activeImageIndex = ref(0); const activeImageIndex = ref(0);
const allImages = computed<MediaItem[]>(() => { const allImages = computed<MediaItem[]>(() => {
@ -40,7 +40,7 @@ const mainImage = computed(() => {
watch(() => props.product, (newProduct) => { watch(() => props.product, (newProduct) => {
if (newProduct) { if (newProduct) {
activeVariant.value = newProduct.variants[0] || null; activeVariant.value = newProduct.variants[0] || null;
activePriceType.value = 'ecer'; activePriceType.value = 'retail';
activeImageIndex.value = 0; activeImageIndex.value = 0;
} else { } else {
activeVariant.value = null; activeVariant.value = null;

View File

@ -20,7 +20,7 @@ import {
} from '@/components/ui/field'; } from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { transfer } from '@/routes/admin/manage/stock-ecer'; import { transfer } from '@/routes/admin/manage/stock-retail';
const open = defineModel<boolean>('open', { default: false }); const open = defineModel<boolean>('open', { default: false });
@ -29,7 +29,7 @@ const props = defineProps<{
variantName: string; variantName: string;
productName: string; productName: string;
stockBagus: number; stockBagus: number;
stockEcer: number; stockRetail: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -116,7 +116,7 @@ function formatNumber(value: number): string {
</div> </div>
<div class="flex justify-between text-xs"> <div class="flex justify-between text-xs">
<span class="text-muted-foreground">Stok Ecer</span> <span class="text-muted-foreground">Stok Ecer</span>
<span class="font-medium tabular-nums text-blue-600">{{ formatNumber(stockEcer) }}</span> <span class="font-medium tabular-nums text-blue-600">{{ formatNumber(stockRetail) }}</span>
</div> </div>
</div> </div>
@ -128,9 +128,9 @@ function formatNumber(value: number): string {
<FieldGroup> <FieldGroup>
<FieldSet class="grid gap-4"> <FieldSet class="grid gap-4">
<Field> <Field>
<FieldLabel for="ecer-quantity" required>Jumlah Transfer</FieldLabel> <FieldLabel for="retail-quantity" required>Jumlah Transfer</FieldLabel>
<NumberInput <NumberInput
id="ecer-quantity" id="retail-quantity"
v-model="form.quantity" v-model="form.quantity"
:max="stockBagus" :max="stockBagus"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
@ -145,9 +145,9 @@ function formatNumber(value: number): string {
</Field> </Field>
<Field> <Field>
<FieldLabel for="ecer-notes">Catatan</FieldLabel> <FieldLabel for="retail-notes">Catatan</FieldLabel>
<Textarea <Textarea
id="ecer-notes" id="retail-notes"
v-model="form.notes" v-model="form.notes"
rows="2" rows="2"
placeholder="Contoh: Transfer untuk kebutuhan kasir" placeholder="Contoh: Transfer untuk kebutuhan kasir"

View File

@ -1,5 +1,5 @@
export const StockQuality = { export const StockQuality = {
GOOD: 'good', GOOD: 'good',
REJECT: 'reject', REJECT: 'reject',
ECER: 'ecer', RETAIL: 'retail',
} as const; } as const;

View File

@ -129,7 +129,7 @@ const getProductImage = (product: Product, indexOffset = 0) => {
// Price Range Resolver // Price Range Resolver
const getProductPriceRange = (product: Product) => { const getProductPriceRange = (product: Product) => {
const prices = product.variants.flatMap(v => 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) { if (prices.length === 0) {

View File

@ -109,7 +109,7 @@ const isCashierUser = computed(() =>
); );
const search = ref(''); const search = ref('');
const selectedStockQuality = ref<'good' | 'reject' | 'ecer'>(isCashierUser.value ? StockQuality.ECER : StockQuality.GOOD); const selectedStockQuality = ref<'good' | 'reject' | 'retail'>(isCashierUser.value ? StockQuality.RETAIL : StockQuality.GOOD);
const cart = ref<OrderCartItem[]>([]); const cart = ref<OrderCartItem[]>([]);
const customerFormOpen = ref(false); const customerFormOpen = ref(false);
const cartDetailOpen = ref(false); const cartDetailOpen = ref(false);
@ -131,7 +131,7 @@ const form = useForm({
customer_id: '', customer_id: '',
marketing_id: defaultMarketingId.value || 'none', marketing_id: defaultMarketingId.value || 'none',
channel: 'store', channel: 'store',
price_type: 'ecer', price_type: 'retail',
payment_type: OrderPaymentType.CASH, payment_type: OrderPaymentType.CASH,
is_affiliate: false, is_affiliate: false,
tiktok_order_id: '', tiktok_order_id: '',
@ -200,7 +200,7 @@ watch(
form.price_type = 'tiktok'; form.price_type = 'tiktok';
form.payment_type = OrderPaymentType.MARKETPLACE; form.payment_type = OrderPaymentType.MARKETPLACE;
} else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) { } else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) {
form.price_type = 'ecer'; form.price_type = 'retail';
} }
if (channel === 'store') { if (channel === 'store') {
@ -250,7 +250,7 @@ function upsertCartItem(item: OrderCartItem) {
function availableStockForVariant(variant: ProductVariantItem, stockQuality: string): number { function availableStockForVariant(variant: ProductVariantItem, stockQuality: string): number {
if (stockQuality === StockQuality.REJECT) return variant.reject_stock; if (stockQuality === StockQuality.REJECT) return variant.reject_stock;
if (stockQuality === StockQuality.ECER) return variant.stock_ecer ?? 0; if (stockQuality === StockQuality.RETAIL) return variant.stock_retail ?? 0;
return variant.stock; return variant.stock;
} }
@ -337,7 +337,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
const availableStock = availableStockForVariant(variant, stockQuality); const availableStock = availableStockForVariant(variant, stockQuality);
if (availableStock < 1) { if (availableStock < 1) {
toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'reject' : stockQuality === StockQuality.ECER ? 'ecer' : 'bagus'} tidak tersedia.`); toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'Reject' : stockQuality === StockQuality.RETAIL ? 'Eceran' : 'Bagus'} tidak tersedia.`);
return; return;
} }
@ -350,7 +350,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1; const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
if (nextQty > availableStock) { if (nextQty > availableStock) {
toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'reject' : stockQuality === StockQuality.ECER ? 'ecer' : 'bagus'} tidak mencukupi.`); toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'Reject' : stockQuality === StockQuality.RETAIL ? 'Eceran' : 'Bagus'} tidak mencukupi.`);
return; return;
} }
@ -467,10 +467,10 @@ function buildFormData(): FormData {
formData.append('marketing_id', String(authUser.value.id)); formData.append('marketing_id', String(authUser.value.id));
} }
// For cashier, force store channel with ecer price type and cash payment // For cashier, force store channel with retail price type and cash payment
if (isCashierUser.value) { if (isCashierUser.value) {
formData.append('channel', 'store'); formData.append('channel', 'store');
formData.append('price_type', 'ecer'); formData.append('price_type', 'retail');
formData.append('payment_type', OrderPaymentType.CASH); formData.append('payment_type', OrderPaymentType.CASH);
} else { } else {
formData.append('channel', form.channel); formData.append('channel', form.channel);
@ -596,7 +596,7 @@ function submit() {
{{ variant.name }} {{ variant.name }}
</p> </p>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
<span v-if="isCashierUser" class="tabular-nums">Ecer: {{ variant.stock_ecer ?? 0 }}</span> <span v-if="isCashierUser" class="tabular-nums">Ecer: {{ variant.stock_retail ?? 0 }}</span>
<template v-else> <template v-else>
<span class="tabular-nums">Bagus: {{ variant.stock }}</span> <span class="tabular-nums">Bagus: {{ variant.stock }}</span>
<span class="mx-1">·</span> <span class="mx-1">·</span>
@ -813,7 +813,7 @@ function submit() {
· ·
{{ item.stock_quality_label ?? (item.stock_quality === {{ item.stock_quality_label ?? (item.stock_quality ===
StockQuality.REJECT StockQuality.REJECT
? 'Reject' : item.stock_quality === StockQuality.ECER ? 'Reject' : item.stock_quality === StockQuality.RETAIL
? 'Eceran' : 'Bagus') }} ? 'Eceran' : 'Bagus') }}
</p> </p>
</div> </div>
@ -951,7 +951,7 @@ function submit() {
<p class="truncate text-xs text-muted-foreground"> <p class="truncate text-xs text-muted-foreground">
{{ item.variant_name }} {{ item.variant_name }}
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject' · {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject'
: item.stock_quality === StockQuality.ECER ? 'Eceran' : 'Bagus') : item.stock_quality === StockQuality.RETAIL ? 'Eceran' : 'Bagus')
}} }}
</p> </p>
</div> </div>

View File

@ -21,7 +21,7 @@ const initialData = computed(() => ({
id: variant.id, id: variant.id,
name: variant.name, name: variant.name,
stock: variant.stock, stock: variant.stock,
stock_ecer: variant.stock_ecer, stock_retail: variant.stock_retail,
images: variant.images ?? [], images: variant.images ?? [],
})), })),
})); }));

View File

@ -33,7 +33,7 @@ const props = withDefaults(
id?: number; id?: number;
name?: string; name?: string;
stock?: number | string; stock?: number | string;
stock_ecer?: number | string; stock_retail?: number | string;
images?: Array<{ id: number; url: string; thumb_url: string }>; images?: Array<{ id: number; url: string; thumb_url: string }>;
}>; }>;
}; };
@ -64,7 +64,7 @@ const {
client_id: createClientId(), client_id: createClientId(),
name: '', name: '',
stock: '0', stock: '0',
stock_ecer: '0', stock_retail: '0',
media: createMediaUploadState(), media: createMediaUploadState(),
}), }),
() => { () => {
@ -73,7 +73,7 @@ const {
client_id: createClientId(), client_id: createClientId(),
name: '', name: '',
stock: '0', stock: '0',
stock_ecer: '0', stock_retail: '0',
media: createMediaUploadState(), media: createMediaUploadState(),
}]; }];
} }
@ -83,7 +83,7 @@ const {
id: variant.id, id: variant.id,
name: variant.name ?? '', name: variant.name ?? '',
stock: variant.stock != null ? String(variant.stock) : '0', 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 ?? []), media: createMediaUploadState(variant.images ?? []),
})); }));
}, },
@ -126,7 +126,7 @@ function buildFormData(): FormData {
appendToFormData(formData, (formData, index, variant) => { appendToFormData(formData, (formData, index, variant) => {
formData.append(`variants[${index}][name]`, variant.name.trim()); 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]`, 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); appendMediaToFormData(formData, `variants[${index}]`, variant.media);
}, props.method); }, props.method);
@ -228,12 +228,12 @@ function submit() {
<FieldError :errors="variantErrors(form, variant.client_id, 'stock')" /> <FieldError :errors="variantErrors(form, variant.client_id, 'stock')" />
</Field> </Field>
<Field> <Field>
<FieldLabel :for="`variant_stock_ecer_${variant.client_id}`" required> <FieldLabel :for="`variant_stock_retail_${variant.client_id}`" required>
Stok Ecer Stok Ecer
</FieldLabel> </FieldLabel>
<NumberInput :id="`variant_stock_ecer_${variant.client_id}`" :model-value="variant.stock_ecer" <NumberInput :id="`variant_stock_retail_${variant.client_id}`" :model-value="variant.stock_retail"
@update:model-value="setVariantField(variant.client_id, 'stock_ecer', String($event))" /> @update:model-value="setVariantField(variant.client_id, 'stock_retail', String($event))" />
<FieldError :errors="variantErrors(form, variant.client_id, 'stock_ecer')" /> <FieldError :errors="variantErrors(form, variant.client_id, 'stock_retail')" />
</Field> </Field>
</FieldSet> </FieldSet>

View File

@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { DataTableEmpty } from '@/components/data-table'; import { DataTableEmpty } from '@/components/data-table';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.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 { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -72,20 +72,20 @@ function rowNumber(index: number): number {
return (props.firstItem ?? 1) + index; return (props.firstItem ?? 1) + index;
} }
// Stock Ecer Transfer Modal // Stock Retail Transfer Modal
const transferModalOpen = ref(false); const transferModalOpen = ref(false);
const transferVariantId = ref(0); const transferVariantId = ref(0);
const transferVariantName = ref(''); const transferVariantName = ref('');
const transferProductName = ref(''); const transferProductName = ref('');
const transferStockBagus = ref(0); const transferStockBagus = ref(0);
const transferStockEcer = ref(0); const transferStockRetail = ref(0);
function openTransferModal(product: ProductListItem, variant: Variant) { function openTransferModal(product: ProductListItem, variant: Variant) {
transferVariantId.value = variant.id; transferVariantId.value = variant.id;
transferVariantName.value = variant.name; transferVariantName.value = variant.name;
transferProductName.value = product.name; transferProductName.value = product.name;
transferStockBagus.value = variant.stock; transferStockBagus.value = variant.stock;
transferStockEcer.value = variant.stock_ecer; transferStockRetail.value = variant.stock_retail;
transferModalOpen.value = true; transferModalOpen.value = true;
} }
@ -134,7 +134,7 @@ function onTransferSubmitted() {
</span> </span>
<span> <span>
Total stok ecer <strong class="text-blue-600"> Total stok ecer <strong class="text-blue-600">
{{product.variants.reduce((acc, v) => acc + v.stock_ecer, 0)}} {{product.variants.reduce((acc, v) => acc + v.stock_retail, 0)}}
</strong> </strong>
</span> </span>
</div> </div>
@ -179,7 +179,7 @@ function onTransferSubmitted() {
{{ formatStock(variant.reject_stock) }} {{ formatStock(variant.reject_stock) }}
</TableCell> </TableCell>
<TableCell class="tabular-nums text-blue-600"> <TableCell class="tabular-nums text-blue-600">
{{ formatStock(variant.stock_ecer) }} {{ formatStock(variant.stock_retail) }}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div v-if="variant.prices?.length" class="space-y-0.5 text-xs"> <div v-if="variant.prices?.length" class="space-y-0.5 text-xs">
@ -236,13 +236,13 @@ function onTransferSubmitted() {
</div> </div>
</div> </div>
<StockEcerTransferModal <StockRetailTransferModal
v-model:open="transferModalOpen" v-model:open="transferModalOpen"
:variant-id="transferVariantId" :variant-id="transferVariantId"
:variant-name="transferVariantName" :variant-name="transferVariantName"
:product-name="transferProductName" :product-name="transferProductName"
:stock-bagus="transferStockBagus" :stock-bagus="transferStockBagus"
:stock-ecer="transferStockEcer" :stock-retail="transferStockRetail"
@submitted="onTransferSubmitted" @submitted="onTransferSubmitted"
/> />
</template> </template>

View File

@ -103,7 +103,7 @@ export const STOCK_QUALITY_OPTIONS = [
] as const; ] as const;
export const STOCK_QUALITY_OPTIONS_CASHIER = [ export const STOCK_QUALITY_OPTIONS_CASHIER = [
{ value: 'ecer', label: 'Eceran' }, { value: 'retail', label: 'Eceran' },
] as const; ] as const;
export type OrderEditItem = { export type OrderEditItem = {

View File

@ -3,7 +3,7 @@ export const PRICE_TYPES = [
'agent', 'agent',
'sub_agent', 'sub_agent',
'grosir', 'grosir',
'ecer', 'retail',
'tiktok', 'tiktok',
'shopee', 'shopee',
'harga_modal', 'harga_modal',
@ -16,7 +16,7 @@ export const PRICE_TYPE_LABELS: Record<PriceType, string> = {
agent: 'Agen', agent: 'Agen',
sub_agent: 'Sub Agen', sub_agent: 'Sub Agen',
grosir: 'Grosir', grosir: 'Grosir',
ecer: 'Eceran', retail: 'Eceran',
tiktok: 'TikTok', tiktok: 'TikTok',
shopee: 'Shopee', shopee: 'Shopee',
harga_modal: 'Harga Modal', harga_modal: 'Harga Modal',
@ -43,7 +43,7 @@ export interface Variant {
name: string; name: string;
stock: number; stock: number;
reject_stock: number; reject_stock: number;
stock_ecer: number; stock_retail: number;
prices: Price[]; prices: Price[];
images: MediaItem[]; images: MediaItem[];
} }
@ -95,6 +95,6 @@ export interface ProductVariantFormItem {
id?: number; id?: number;
name: string; name: string;
stock: string | number; stock: string | number;
stock_ecer: string | number; stock_retail: string | number;
media: import('@/types/media').MediaUploadState; media: import('@/types/media').MediaUploadState;
} }

View File

@ -14,16 +14,16 @@
use App\Http\Controllers\Admin\Hr\AttendanceController; use App\Http\Controllers\Admin\Hr\AttendanceController;
use App\Http\Controllers\Admin\Hr\EmployeeController; use App\Http\Controllers\Admin\Hr\EmployeeController;
use App\Http\Controllers\Admin\Hr\LeaveRequestController; use App\Http\Controllers\Admin\Hr\LeaveRequestController;
use App\Http\Controllers\Admin\Manage\CuttingController; use App\Http\Controllers\Admin\Manage\Cutting\CuttingController;
use App\Http\Controllers\Admin\Manage\CuttingDraftItemController; use App\Http\Controllers\Admin\Manage\Cutting\CuttingDraftItemController;
use App\Http\Controllers\Admin\Manage\OrderController; use App\Http\Controllers\Admin\Manage\Order\OrderController;
use App\Http\Controllers\Admin\Manage\OrderDraftItemController; use App\Http\Controllers\Admin\Manage\Order\OrderDraftItemController;
use App\Http\Controllers\Admin\Manage\OwnerVerificationController; use App\Http\Controllers\Admin\Manage\OwnerVerificationController;
use App\Http\Controllers\Admin\Manage\PurchaseController; use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController;
use App\Http\Controllers\Admin\Manage\PurchaseDraftItemController; use App\Http\Controllers\Admin\Manage\Purchase\PurchaseDraftItemController;
use App\Http\Controllers\Admin\Manage\StokOpnameController; use App\Http\Controllers\Admin\Manage\StokOpnameController;
use App\Http\Controllers\Admin\Manage\StockController; use App\Http\Controllers\Admin\Manage\Stock\StockController;
use App\Http\Controllers\Admin\Manage\StockEcerController; use App\Http\Controllers\Admin\Manage\Stock\StockRetailController;
use App\Http\Controllers\Admin\Master\CategoryController; use App\Http\Controllers\Admin\Master\CategoryController;
use App\Http\Controllers\Admin\Master\CustomerController; use App\Http\Controllers\Admin\Master\CustomerController;
use App\Http\Controllers\Admin\Master\ProductController; use App\Http\Controllers\Admin\Master\ProductController;
@ -357,10 +357,10 @@
->name('verify'); ->name('verify');
}); });
Route::prefix('stock-ecer')->name('stock-ecer.') Route::prefix('stock-retail')->name('stock-retail.')
->middleware('permission:'.Permission::STOCKS_VIEW->value) ->middleware('permission:'.Permission::STOCKS_VIEW->value)
->group(function () { ->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.') Route::prefix('stok-opnames')->name('stok-opnames.')