feat: enhance controller best practices and introduce new Cutting and Order management features with updated services and validation handling
This commit is contained in:
parent
393c36e932
commit
7a1c4efe4a
115
BEST_PRACTICE.md
115
BEST_PRACTICE.md
@ -169,6 +169,121 @@ ### Model Eloquent
|
||||
}
|
||||
```
|
||||
|
||||
### Controller
|
||||
|
||||
- **Tugas Controller:**
|
||||
- Controller hanya menangani **request dan response**.
|
||||
- Semua logika bisnis (validasi aturan bisnis, query kompleks, transformasi data, authorization bisnis) **wajib** dipindahkan ke **Service**.
|
||||
- Controller memanggil service, lalu mengembalikan `Inertia::render()`, `redirect()`, atau `response()->json()`.
|
||||
|
||||
- **Urutan Method:**
|
||||
1. `index`
|
||||
2. `create`
|
||||
3. `store`
|
||||
4. `show` (jika ada)
|
||||
5. `edit`
|
||||
6. `update`
|
||||
7. `destroy`
|
||||
8. Method tambahan (custom action) ditempatkan **di bawah** `destroy`.
|
||||
|
||||
- **Form Request:**
|
||||
- **Selalu** gunakan Form Request untuk validasi, sekecil apapun validasinya.
|
||||
- Authorization di Form Request menggunakan `$this->user()`, bukan helper global.
|
||||
|
||||
- **Authenticated User:**
|
||||
- **Selalu** ambil user dari `$request->user()` pada method controller.
|
||||
- **Jangan** gunakan `request()->user()`, `auth()->user()`, atau `Auth::user()`.
|
||||
|
||||
- **Docblock:**
|
||||
- **Jangan** gunakan PHPDoc/docblock pada controller.
|
||||
|
||||
- **Penamaan:**
|
||||
- Variable, method, file, dan elemen sistem lainnya **wajib** menggunakan **Bahasa Inggris**.
|
||||
- Teks yang ditampilkan ke user (flash message, label UI) menggunakan **Bahasa Indonesia**.
|
||||
- Kecuali nama modul yang memang sudah Bahasa Inggris (misal: `customer`, `cutting`, `order`).
|
||||
|
||||
- **Flash Message:**
|
||||
- Gunakan helper dari trait `FlashesEntityMessage` untuk pesan standar CRUD:
|
||||
- `flashCreated($entity)` → "{entity} berhasil ditambahkan."
|
||||
- `flashUpdated($entity)` → "{entity} berhasil diperbarui."
|
||||
- `flashDeleted($entity)` → "{entity} berhasil dihapus."
|
||||
- `flashStatusUpdated($entity)` → "Status {entity} berhasil diperbarui."
|
||||
- Gunakan `flashSuccess($message)` **hanya** untuk pesan custom (verifikasi owner, approve/reject, transisi status, dll.).
|
||||
|
||||
- **Struktur Folder:**
|
||||
- Jika satu modul memiliki **lebih dari satu controller**, kelompokkan dalam subfolder modul.
|
||||
- **Contoh:**
|
||||
```
|
||||
app/Http/Controllers/Admin/Manage/
|
||||
Cutting/
|
||||
CuttingController.php
|
||||
CuttingDraftItemController.php
|
||||
Order/
|
||||
OrderController.php
|
||||
OrderDraftItemController.php
|
||||
Purchase/
|
||||
PurchaseController.php
|
||||
PurchaseDraftItemController.php
|
||||
Stock/
|
||||
StockController.php
|
||||
StockRetailController.php
|
||||
StokOpnameController.php ← modul dengan 1 controller tetap di level parent
|
||||
OwnerVerificationController.php
|
||||
```
|
||||
|
||||
- **Contoh:**
|
||||
```php
|
||||
namespace App\Http\Controllers\Admin\Manage\Order;
|
||||
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\OrderRequest;
|
||||
use App\Models\Order;
|
||||
use App\Services\Manage\OrderService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
use FlashesEntityMessage, ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly OrderService $orderService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
|
||||
return Inertia::render('admin/manage/orders/Index', [
|
||||
'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(OrderRequest $request): RedirectResponse
|
||||
{
|
||||
$this->orderService->create($request->validated(), $request->user());
|
||||
|
||||
$this->flashCreated('Pesanan');
|
||||
|
||||
return redirect()->route('admin.manage.orders.index');
|
||||
}
|
||||
|
||||
public function destroy(Order $order): RedirectResponse
|
||||
{
|
||||
$this->orderService->delete($order);
|
||||
|
||||
$this->flashDeleted('Pesanan');
|
||||
|
||||
return redirect()->route('admin.manage.orders.index');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database & Migrasi
|
||||
|
||||
### Struktur File Migrasi
|
||||
|
||||
@ -13,7 +13,7 @@ enum OwnerVerificationAction: string
|
||||
case DELETE = 'delete';
|
||||
case TOGGLE_STATUS = 'toggle_status';
|
||||
case STOCK_VERIFY = 'stock_verify';
|
||||
case STOCK_ECER_TRANSFER = 'stock_ecer_transfer';
|
||||
case STOCK_RETAIL_TRANSFER = 'stock_retail_transfer';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@ -23,7 +23,7 @@ public function label(): string
|
||||
self::DELETE => 'Hapus',
|
||||
self::TOGGLE_STATUS => 'Ubah Status',
|
||||
self::STOCK_VERIFY => 'Verifikasi Stok',
|
||||
self::STOCK_ECER_TRANSFER => 'Transfer Stok Ecer',
|
||||
self::STOCK_RETAIL_TRANSFER => 'Transfer Stok Ecer',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ enum PriceType: string
|
||||
case AGENT = 'agent';
|
||||
case SUB_AGENT = 'sub_agent';
|
||||
case GROSIR = 'grosir';
|
||||
case ECER = 'ecer';
|
||||
case RETAIL = 'retail';
|
||||
case TIKTOK = 'tiktok';
|
||||
case SHOPEE = 'shopee';
|
||||
case HARGA_MODAL = 'harga_modal';
|
||||
@ -24,7 +24,7 @@ public function label(): string
|
||||
self::AGENT => 'Agen',
|
||||
self::SUB_AGENT => 'Sub Agen',
|
||||
self::GROSIR => 'Grosir',
|
||||
self::ECER => 'Eceran',
|
||||
self::RETAIL => 'Eceran',
|
||||
self::TIKTOK => 'TikTok',
|
||||
self::SHOPEE => 'Shopee',
|
||||
self::HARGA_MODAL => 'Harga Modal',
|
||||
|
||||
@ -10,14 +10,14 @@ enum ProductStockQuality: string
|
||||
|
||||
case GOOD = 'good';
|
||||
case REJECT = 'reject';
|
||||
case ECER = 'ecer';
|
||||
case RETAIL = 'retail';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::GOOD => 'Bagus',
|
||||
self::REJECT => 'Reject',
|
||||
self::ECER => 'Eceran',
|
||||
self::RETAIL => 'Eceran',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Account\UpdateAppearanceRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
@ -14,10 +15,10 @@ class AppearanceController extends Controller
|
||||
{
|
||||
use FlashesEntityMessage;
|
||||
|
||||
public function edit(): Response
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
return Inertia::render('admin/account/Appearance', [
|
||||
'appearance' => request()->cookie('appearance', 'system'),
|
||||
'appearance' => $request->cookie('appearance', 'system'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -7,8 +7,9 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Account\UpdateProfileRequest;
|
||||
use App\Services\Account\ProfileService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use App\Services\Hr\EmployeeService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@ -18,18 +19,17 @@ class ProfileController extends Controller
|
||||
|
||||
public function __construct(
|
||||
private readonly ProfileService $profileService,
|
||||
private readonly EmployeeService $employeeService,
|
||||
) {}
|
||||
|
||||
public function edit(): Response
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$user = auth()->user()->load('profile');
|
||||
$editData = $this->employeeService->findForEdit($request->user());
|
||||
|
||||
return Inertia::render('admin/account/Profile', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'user' => $user,
|
||||
'profilePhoto' => $user->profile
|
||||
? MediaPresenter::first($user->profile, 'profile_photo')
|
||||
: null,
|
||||
'user' => $editData['employee'],
|
||||
'profilePhoto' => $editData['profilePhoto'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\System\AnalysisService;
|
||||
use Carbon\Carbon;
|
||||
@ -20,9 +19,7 @@ public function index(Request $request): Response
|
||||
{
|
||||
$startDate = $request->query('start_date') ? Carbon::parse($request->query('start_date'))->startOfDay() : null;
|
||||
$endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
|
||||
|
||||
$user = $request->user();
|
||||
$isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
|
||||
|
||||
return Inertia::render('admin/Analysis', [
|
||||
'filters' => [
|
||||
@ -31,7 +28,7 @@ public function index(Request $request): Response
|
||||
],
|
||||
'attendance' => $this->analysisService->getAttendance(),
|
||||
'myAttendance' => $user ? $this->analysisService->getMyAttendance($user, $startDate, $endDate) : null,
|
||||
'isManager' => $isManager,
|
||||
'isManager' => $this->analysisService->isManager($user),
|
||||
'cashOverview' => $this->analysisService->getCashOverview(),
|
||||
'rawMaterialStock' => $this->analysisService->getRawMaterialStock(),
|
||||
'productStock' => $this->analysisService->getProductStock(),
|
||||
|
||||
@ -38,6 +38,24 @@ public function index(Request $request): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
|
||||
{
|
||||
$this->cashService->updateDeposit($cashTransaction, $request->validated());
|
||||
|
||||
$this->flashUpdated('Setor kas');
|
||||
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
|
||||
public function destroy(CashTransaction $cashTransaction): RedirectResponse
|
||||
{
|
||||
$this->cashService->deleteTransaction($cashTransaction);
|
||||
|
||||
$this->flashDeleted('Setor kas');
|
||||
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
|
||||
public function deposit(DepositRequest $request): RedirectResponse
|
||||
{
|
||||
$cashAccount = $this->cashService->getDefaultAccount();
|
||||
@ -59,22 +77,4 @@ public function withdraw(WithdrawRequest $request): RedirectResponse
|
||||
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
|
||||
public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
|
||||
{
|
||||
$this->cashService->updateDeposit($cashTransaction, $request->validated());
|
||||
|
||||
$this->flashSuccess('Setor kas berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
|
||||
public function destroy(CashTransaction $cashTransaction): RedirectResponse
|
||||
{
|
||||
$this->cashService->deleteTransaction($cashTransaction);
|
||||
|
||||
$this->flashSuccess('Setor kas berhasil dihapus.');
|
||||
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Finance;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Finance\ApproveEmployeeAdvanceRequest;
|
||||
use App\Http\Requests\Admin\Finance\DestroyEmployeeAdvanceRequest;
|
||||
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
||||
use App\Http\Requests\Admin\Finance\PayEmployeeAdvanceRequest;
|
||||
use App\Http\Requests\Admin\Finance\RejectEmployeeAdvanceRequest;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Services\Finance\EmployeeAdvanceService;
|
||||
@ -27,15 +29,10 @@ public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$status = $request->string('status')->toString();
|
||||
$user = $request->user();
|
||||
$pageData = $this->employeeAdvanceService->indexPageData($tableQuery, $request->user(), $status);
|
||||
|
||||
return Inertia::render('admin/finance/employee-advances/Index', [
|
||||
'employeeAdvances' => $this->employeeAdvanceService->paginateForIndex($tableQuery, $user, $status),
|
||||
'summary' => $this->employeeAdvanceService->outstandingSummary($user),
|
||||
'authEmployeeId' => $user?->employee?->id,
|
||||
'canSubmit' => $user?->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
|
||||
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
|
||||
&& $user->employee !== null,
|
||||
...$pageData,
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
'status' => $status,
|
||||
]),
|
||||
@ -60,18 +57,18 @@ public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employe
|
||||
return redirect()->route('admin.finance.employee_advances.index');
|
||||
}
|
||||
|
||||
public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
public function destroy(DestroyEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
{
|
||||
$this->employeeAdvanceService->delete($employeeAdvance, auth()->user());
|
||||
$this->employeeAdvanceService->delete($employeeAdvance, $request->user());
|
||||
|
||||
$this->flashDeleted('Kasbon');
|
||||
|
||||
return redirect()->route('admin.finance.employee_advances.index');
|
||||
}
|
||||
|
||||
public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
public function approve(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
{
|
||||
$this->employeeAdvanceService->approve($employeeAdvance, auth()->user());
|
||||
$this->employeeAdvanceService->approve($employeeAdvance, $request->user());
|
||||
|
||||
$this->flashSuccess('Kasbon berhasil disetujui dan dicairkan dari kas.');
|
||||
|
||||
@ -83,7 +80,7 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
|
||||
$this->employeeAdvanceService->reject(
|
||||
$employeeAdvance,
|
||||
$request->validated('reason'),
|
||||
auth()->user()
|
||||
$request->user(),
|
||||
);
|
||||
|
||||
$this->flashSuccess('Kasbon berhasil ditolak.');
|
||||
@ -91,11 +88,11 @@ public function reject(RejectEmployeeAdvanceRequest $request, EmployeeAdvance $e
|
||||
return redirect()->route('admin.finance.employee_advances.index');
|
||||
}
|
||||
|
||||
public function pay(Request $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
public function pay(PayEmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||
{
|
||||
$payAmount = $request->input('amount') ? (int) $request->input('amount') : null;
|
||||
|
||||
$this->employeeAdvanceService->pay($employeeAdvance, auth()->user(), $payAmount);
|
||||
$this->employeeAdvanceService->pay($employeeAdvance, $request->user(), $payAmount);
|
||||
|
||||
$this->flashSuccess('Kasbon berhasil dibayar.');
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
class PayrollController extends Controller
|
||||
{
|
||||
use FlashesEntityMessage,ParsesDataTableQuery;
|
||||
use FlashesEntityMessage, ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly PayrollService $payrollService,
|
||||
@ -28,15 +28,16 @@ public function index(Request $request): Response
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$periodId = $request->integer('period_id') ?: null;
|
||||
$period = $this->payrollService->resolvePeriod($periodId);
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('admin/finance/payroll/Index', [
|
||||
'periods' => $this->payrollService->listPeriods(),
|
||||
'currentPeriod' => $period,
|
||||
'payrolls' => $period
|
||||
? $this->payrollService->paginateForPeriod($period, $tableQuery, auth()->user())
|
||||
? $this->payrollService->paginateForPeriod($period, $tableQuery, $user)
|
||||
: null,
|
||||
'summary' => $period
|
||||
? $this->payrollService->periodSummary($period, auth()->user())
|
||||
? $this->payrollService->periodSummary($period, $user)
|
||||
: null,
|
||||
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
@ -50,10 +51,10 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr
|
||||
$this->payrollService->addAdjustment(
|
||||
$payroll,
|
||||
$request->validated(),
|
||||
auth()->user(),
|
||||
$request->user(),
|
||||
);
|
||||
|
||||
$this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.');
|
||||
$this->flashCreated('Penyesuaian gaji');
|
||||
|
||||
return redirect()->route('admin.finance.payroll.index', [
|
||||
'period_id' => $payroll->payroll_period_id,
|
||||
@ -65,10 +66,10 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
|
||||
$this->payrollService->updateAdjustment(
|
||||
$payrollAdjustment,
|
||||
$request->validated(),
|
||||
auth()->user(),
|
||||
$request->user(),
|
||||
);
|
||||
|
||||
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
|
||||
$this->flashUpdated('Penyesuaian gaji');
|
||||
|
||||
return redirect()->route('admin.finance.payroll.index', [
|
||||
'period_id' => $payrollAdjustment->payroll->payroll_period_id,
|
||||
@ -81,7 +82,7 @@ public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): Redirec
|
||||
|
||||
$this->payrollService->deleteAdjustment($payrollAdjustment);
|
||||
|
||||
$this->flashSuccess('Penyesuaian gaji berhasil dihapus.');
|
||||
$this->flashDeleted('Penyesuaian gaji');
|
||||
|
||||
return redirect()->route('admin.finance.payroll.index', [
|
||||
'period_id' => $payroll->payroll_period_id,
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\Role;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Hr\Attendance\CheckInRequest;
|
||||
@ -25,37 +23,14 @@ public function __construct(
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$employee = $user?->employee;
|
||||
$isManager = $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
|
||||
|
||||
$scopedEmployeeId = $isManager ? null : $employee?->id;
|
||||
$hasScopedAccess = $isManager || $employee !== null;
|
||||
|
||||
$start = $request->date('start') ?? now()->startOfMonth();
|
||||
$end = $request->date('end') ?? now()->endOfMonth();
|
||||
|
||||
return Inertia::render('admin/hr/attendances/Index', [
|
||||
'attendances' => $this->attendanceService->listForCalendar(
|
||||
$start,
|
||||
$end,
|
||||
$scopedEmployeeId,
|
||||
$hasScopedAccess,
|
||||
),
|
||||
'todayAttendance' => $employee
|
||||
? $this->attendanceService->todayAttendanceForEmployee($employee)
|
||||
: null,
|
||||
'isOnLeave' => $employee
|
||||
? $this->attendanceService->isOnLeaveToday($employee)
|
||||
: false,
|
||||
'canCheckIn' => ($user?->can(Permission::ATTENDANCES_CREATE->value) ?? false)
|
||||
&& $employee !== null,
|
||||
'canManageAll' => $isManager,
|
||||
'calendarRange' => [
|
||||
'start' => $start->toDateString(),
|
||||
'end' => $end->toDateString(),
|
||||
],
|
||||
]);
|
||||
return Inertia::render('admin/hr/attendances/Index', $this->attendanceService->indexPageData(
|
||||
$request->user(),
|
||||
$start,
|
||||
$end,
|
||||
));
|
||||
}
|
||||
|
||||
public function checkIn(CheckInRequest $request): RedirectResponse
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Enums\EmploymentStatus;
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\Role;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -12,7 +11,6 @@
|
||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Hr\EmployeeService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -26,11 +24,6 @@ public function __construct(
|
||||
private readonly EmployeeService $employeeService,
|
||||
) {}
|
||||
|
||||
private function assignableRoleOptions(): array
|
||||
{
|
||||
return Role::assignableSelectOptions();
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
@ -53,7 +46,7 @@ public function index(Request $request): Response
|
||||
'employment_status' => $employmentStatus,
|
||||
'is_active' => $isActive,
|
||||
]),
|
||||
'roles' => $this->assignableRoleOptions(),
|
||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
||||
'genders' => Gender::selectOptions(),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
]);
|
||||
@ -64,7 +57,7 @@ public function create(): Response
|
||||
return Inertia::render('admin/hr/employees/Create', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
'roles' => $this->assignableRoleOptions(),
|
||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -79,16 +72,14 @@ public function store(EmployeeRequest $request): RedirectResponse
|
||||
|
||||
public function edit(User $user): Response
|
||||
{
|
||||
$user->load(['profile', 'employee', 'roles']);
|
||||
$editData = $this->employeeService->findForEdit($user);
|
||||
|
||||
return Inertia::render('admin/hr/employees/Edit', [
|
||||
'genders' => Gender::selectOptions(),
|
||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||
'roles' => $this->assignableRoleOptions(),
|
||||
'employee' => $user,
|
||||
'profilePhoto' => $user->profile
|
||||
? MediaPresenter::first($user->profile, 'profile_photo')
|
||||
: null,
|
||||
'roles' => $this->employeeService->assignableRoleOptions(),
|
||||
'employee' => $editData['employee'],
|
||||
'profilePhoto' => $editData['profilePhoto'],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -96,25 +87,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->employeeService->update($user, $request->validated());
|
||||
|
||||
$this->flashSuccess('Data pegawai berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->employeeService->toggleStatus($user, $request->validated());
|
||||
|
||||
$this->flashSuccess('Status pegawai berhasil diubah.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->employeeService->resetPassword($user);
|
||||
|
||||
$this->flashSuccess('Kata sandi pegawai berhasil direset.');
|
||||
$this->flashUpdated('Pegawai');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
@ -127,4 +100,22 @@ public function destroy(User $user): RedirectResponse
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
$this->employeeService->toggleStatus($user, $request->validated());
|
||||
|
||||
$this->flashStatusUpdated('Pegawai');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
$this->employeeService->resetPassword($user);
|
||||
|
||||
$this->flashSuccess('Kata sandi pegawai berhasil direset.');
|
||||
|
||||
return redirect()->route('admin.hr.employees.index');
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Hr;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Hr\ApproveLeaveRequestRequest;
|
||||
use App\Http\Requests\Admin\Hr\RejectLeaveRequestRequest;
|
||||
use App\Http\Requests\Admin\Hr\SubmitLeaveRequest;
|
||||
use App\Models\LeaveRequest;
|
||||
@ -26,23 +26,10 @@ public function __construct(
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$user = $request->user();
|
||||
|
||||
$hasPending = false;
|
||||
if ($user?->employee !== null) {
|
||||
$hasPending = LeaveRequest::query()
|
||||
->pending()
|
||||
->where('employee_id', $user->employee->id)
|
||||
->exists();
|
||||
}
|
||||
$pageData = $this->leaveRequestService->indexPageData($tableQuery, $request->user());
|
||||
|
||||
return Inertia::render('admin/hr/leave-requests/Index', [
|
||||
'leaveRequests' => $this->leaveRequestService->paginateForIndex($tableQuery, $user),
|
||||
'authEmployeeId' => $user?->employee?->id,
|
||||
'canSubmit' => $user?->can(Permission::LEAVE_REQUESTS_CREATE->value)
|
||||
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
|
||||
&& $user->employee !== null,
|
||||
'hasPending' => $hasPending,
|
||||
...$pageData,
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
]);
|
||||
}
|
||||
@ -67,16 +54,16 @@ public function update(SubmitLeaveRequest $request, LeaveRequest $leaveRequest):
|
||||
|
||||
public function destroy(LeaveRequest $leaveRequest): RedirectResponse
|
||||
{
|
||||
$this->leaveRequestService->delete($leaveRequest, auth()->user());
|
||||
$this->leaveRequestService->delete($leaveRequest);
|
||||
|
||||
$this->flashDeleted('Pengajuan cuti');
|
||||
|
||||
return redirect()->route('admin.hr.leave_requests.index');
|
||||
}
|
||||
|
||||
public function approve(LeaveRequest $leaveRequest): RedirectResponse
|
||||
public function approve(ApproveLeaveRequestRequest $request, LeaveRequest $leaveRequest): RedirectResponse
|
||||
{
|
||||
$this->leaveRequestService->approve($leaveRequest, auth()->user());
|
||||
$this->leaveRequestService->approve($leaveRequest, $request->user());
|
||||
|
||||
$this->flashSuccess('Pengajuan cuti berhasil disetujui.');
|
||||
|
||||
@ -88,7 +75,7 @@ public function reject(RejectLeaveRequestRequest $request, LeaveRequest $leaveRe
|
||||
$this->leaveRequestService->reject(
|
||||
$leaveRequest,
|
||||
$request->validated('reason'),
|
||||
auth()->user(),
|
||||
$request->user(),
|
||||
);
|
||||
|
||||
$this->flashSuccess('Pengajuan cuti berhasil ditolak.');
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Cutting;
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Services\Manage\CuttingService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
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', [
|
||||
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user),
|
||||
@ -57,8 +58,10 @@ public function store(CuttingRequest $request): RedirectResponse
|
||||
|
||||
public function edit(Cutting $cutting): Response|RedirectResponse
|
||||
{
|
||||
if (! $this->cuttingService->isEditable($cutting->status)) {
|
||||
$this->flashError('Proses cutting tidak dapat diubah.');
|
||||
try {
|
||||
$this->cuttingService->ensureEditable($cutting);
|
||||
} catch (ValidationException $exception) {
|
||||
$this->flashError($exception->validator->errors()->first('status'));
|
||||
|
||||
return redirect()->route('admin.manage.cuttings.index');
|
||||
}
|
||||
@ -102,15 +105,7 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
|
||||
$request->validated('result_prices'),
|
||||
);
|
||||
|
||||
$message = match ($status) {
|
||||
CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
|
||||
CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
|
||||
CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
|
||||
CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
|
||||
default => 'Status proses cutting berhasil diperbarui.',
|
||||
};
|
||||
|
||||
$this->flashSuccess($message);
|
||||
$this->flashSuccess($this->cuttingService->transitionStatusMessage($status));
|
||||
|
||||
return redirect()->route('admin.manage.cuttings.index');
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Cutting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\CuttingDraftMaterialRequest;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Order;
|
||||
|
||||
use App\Enums\OrderChannel;
|
||||
use App\Enums\OrderStatus;
|
||||
@ -14,6 +14,7 @@
|
||||
use App\Services\Manage\OrderService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
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
|
||||
{
|
||||
$user = $request->user();
|
||||
@ -73,10 +67,19 @@ public function store(OrderRequest $request): RedirectResponse
|
||||
return redirect()->route('admin.manage.orders.index', $params);
|
||||
}
|
||||
|
||||
public function show(Order $order): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/orders/Show', [
|
||||
'order' => $this->orderService->findForShow($order),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Order $order): Response|RedirectResponse
|
||||
{
|
||||
if (! $this->orderService->isEditable($order->status)) {
|
||||
$this->flashError('Pesanan tidak dapat diubah.');
|
||||
try {
|
||||
$this->orderService->ensureEditable($order);
|
||||
} catch (ValidationException $exception) {
|
||||
$this->flashError($exception->validator->errors()->first('status'));
|
||||
|
||||
return redirect()->route('admin.manage.orders.index');
|
||||
}
|
||||
@ -116,14 +119,7 @@ public function transitionStatus(OrderStatusTransitionRequest $request, Order $o
|
||||
|
||||
$this->orderService->transitionStatus($order, $status);
|
||||
|
||||
$message = match ($status) {
|
||||
OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
|
||||
OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
|
||||
OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
|
||||
default => 'Status pesanan berhasil diperbarui.',
|
||||
};
|
||||
|
||||
$this->flashSuccess($message);
|
||||
$this->flashSuccess($this->orderService->transitionStatusMessage($status));
|
||||
|
||||
return redirect()->route('admin.manage.orders.index');
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Order;
|
||||
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -1,6 +1,6 @@
|
||||
<?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\ParsesDataTableQuery;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Purchase;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\PurchaseDraftItemRequest;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Stock;
|
||||
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -1,23 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Manage;
|
||||
namespace App\Http\Controllers\Admin\Manage\Stock;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\StockEcerTransferRequest;
|
||||
use App\Services\Manage\StockEcerService;
|
||||
use App\Http\Requests\Admin\Manage\StockRetailTransferRequest;
|
||||
use App\Services\Manage\StockRetailService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class StockEcerController extends Controller
|
||||
class StockRetailController extends Controller
|
||||
{
|
||||
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();
|
||||
|
||||
$this->stockEcerService->transfer(
|
||||
$this->stockRetailService->transfer(
|
||||
$validated['product_variant_id'],
|
||||
$validated['quantity'],
|
||||
$request->user(),
|
||||
@ -5,12 +5,14 @@
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Manage\StokOpnameAutoSaveRequest;
|
||||
use App\Http\Requests\Admin\Manage\StokOpnameRejectRequest;
|
||||
use App\Http\Requests\Admin\Manage\StokOpnameRequest;
|
||||
use App\Http\Requests\Admin\Manage\StokOpnameSubmitRequest;
|
||||
use App\Http\Requests\Admin\Manage\StokOpnameVerifyRequest;
|
||||
use App\Models\StokOpname;
|
||||
use App\Services\Manage\StokOpnameService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -50,36 +52,6 @@ public function store(StokOpnameRequest $request): RedirectResponse
|
||||
return redirect()->route('admin.manage.stok-opnames.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-save draft via AJAX (creates or updates silently).
|
||||
*/
|
||||
public function autoSave(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user->can(\App\Enums\Permission::STOK_OPNAMES_CREATE->value) &&
|
||||
! $user->can(\App\Enums\Permission::STOK_OPNAMES_UPDATE->value)) {
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'stok_opname_id' => ['nullable', 'integer', 'exists:stok_opnames,id'],
|
||||
'opname_date' => ['required', 'date'],
|
||||
'notes' => ['nullable', 'string', 'max:1000'],
|
||||
'items' => ['nullable', 'array'],
|
||||
'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'],
|
||||
'items.*.physical_stock' => ['nullable', 'integer', 'min:0'],
|
||||
'items.*.notes' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
|
||||
$stokOpname = $this->stokOpnameService->autoSave($validated, $user);
|
||||
|
||||
return response()->json([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'message' => 'Draft tersimpan otomatis.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(StokOpname $stokOpname): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/stok-opnames/Edit', [
|
||||
@ -106,6 +78,16 @@ public function destroy(StokOpname $stokOpname): RedirectResponse
|
||||
return redirect()->route('admin.manage.stok-opnames.index');
|
||||
}
|
||||
|
||||
public function autoSave(StokOpnameAutoSaveRequest $request): JsonResponse
|
||||
{
|
||||
$stokOpname = $this->stokOpnameService->autoSave($request->validated(), $request->user());
|
||||
|
||||
return response()->json([
|
||||
'stok_opname_id' => $stokOpname->id,
|
||||
'message' => 'Draft tersimpan otomatis.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit(StokOpnameSubmitRequest $request, StokOpname $stokOpname): RedirectResponse
|
||||
{
|
||||
$this->stokOpnameService->submit($stokOpname, $request->user());
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
use FlashesEntityMessage,ParsesDataTableQuery;
|
||||
use FlashesEntityMessage, ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly CategoryService $categoryService,
|
||||
|
||||
@ -41,18 +41,6 @@ public function store(CustomerRequest $request): RedirectResponse
|
||||
return redirect()->route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function storeApi(CustomerRequest $request): JsonResponse
|
||||
{
|
||||
$this->customerService->create($request->validated());
|
||||
|
||||
$customer = Customer::query()->latest()->first();
|
||||
|
||||
return response()->json([
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
|
||||
{
|
||||
$this->customerService->update($customer, $request->validated());
|
||||
@ -70,4 +58,14 @@ public function destroy(Customer $customer): RedirectResponse
|
||||
|
||||
return redirect()->route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function storeApi(CustomerRequest $request): JsonResponse
|
||||
{
|
||||
$customer = $this->customerService->createAndReturn($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'id' => $customer->id,
|
||||
'name' => $customer->name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,8 @@
|
||||
use App\Http\Requests\Admin\Master\ProductRequest;
|
||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Services\Master\CategoryService;
|
||||
use App\Services\Master\ProductService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -62,20 +60,9 @@ public function store(ProductRequest $request): RedirectResponse
|
||||
|
||||
public function edit(Product $product): Response
|
||||
{
|
||||
$product->load([
|
||||
'categories',
|
||||
'variants' => fn ($query) => $query
|
||||
->with('media')
|
||||
->orderBy('created_at'),
|
||||
]);
|
||||
|
||||
$product->variants->each(function (ProductVariant $variant): void {
|
||||
$variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
|
||||
});
|
||||
|
||||
return Inertia::render('admin/master/products/Edit', [
|
||||
'categories' => $this->categoryService->getSelectOptions(),
|
||||
'product' => $product,
|
||||
'product' => $this->productService->findForEdit($product),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -88,15 +75,6 @@ public function update(ProductRequest $request, Product $product): RedirectRespo
|
||||
return redirect()->route('admin.master.products.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, Product $product): RedirectResponse
|
||||
{
|
||||
$this->productService->toggleStatus($product, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan status produk berhasil diajukan dan menunggu verifikasi owner.');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Product $product): RedirectResponse
|
||||
{
|
||||
$this->productService->delete($product, $request->user());
|
||||
@ -105,4 +83,13 @@ public function destroy(Request $request, Product $product): RedirectResponse
|
||||
|
||||
return redirect()->route('admin.master.products.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, Product $product): RedirectResponse
|
||||
{
|
||||
$this->productService->toggleStatus($product, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan status produk berhasil diajukan dan menunggu verifikasi owner.');
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,7 @@
|
||||
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Master\RawMaterialService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@ -58,17 +56,9 @@ public function store(RawMaterialRequest $request): RedirectResponse
|
||||
|
||||
public function edit(RawMaterial $rawMaterial): Response
|
||||
{
|
||||
$rawMaterial->load([
|
||||
'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
|
||||
]);
|
||||
|
||||
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
|
||||
$price->setAttribute('images', MediaPresenter::collection($price, 'images'));
|
||||
});
|
||||
|
||||
return Inertia::render('admin/master/raw-materials/Edit', [
|
||||
'units' => RawMaterialUnit::selectOptions(),
|
||||
'rawMaterial' => $rawMaterial,
|
||||
'rawMaterial' => $this->rawMaterialService->findForEdit($rawMaterial),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -81,15 +71,6 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
|
||||
return redirect()->route('admin.master.raw_materials.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
$this->rawMaterialService->toggleStatus($rawMaterial, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function destroy(Request $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
$this->rawMaterialService->delete($rawMaterial, $request->user());
|
||||
@ -98,4 +79,13 @@ public function destroy(Request $request, RawMaterial $rawMaterial): RedirectRes
|
||||
|
||||
return redirect()->route('admin.master.raw_materials.index');
|
||||
}
|
||||
|
||||
public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
$this->rawMaterialService->toggleStatus($rawMaterial, $request->validated(), $request->user());
|
||||
|
||||
$this->flashSuccess('Perubahan status bahan baku berhasil diajukan dan menunggu verifikasi owner.');
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,77 +4,47 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Notification;
|
||||
use App\Services\System\NotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NotificationService $notificationService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$notifications = Notification::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest()
|
||||
->limit(20)
|
||||
->get()
|
||||
->map(fn (Notification $n) => [
|
||||
'id' => $n->id,
|
||||
'title' => $n->title,
|
||||
'body' => $n->body,
|
||||
'url' => $n->url,
|
||||
'is_read' => $n->is_read,
|
||||
'created_at' => $n->created_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
$unreadCount = Notification::query()
|
||||
->where('user_id', $user->id)
|
||||
->unread()
|
||||
->count();
|
||||
|
||||
return response()->json([
|
||||
'notifications' => $notifications,
|
||||
'unread_count' => $unreadCount,
|
||||
]);
|
||||
return response()->json(
|
||||
$this->notificationService->listForUser($request->user()),
|
||||
);
|
||||
}
|
||||
|
||||
public function markAsRead(Request $request, Notification $notification): JsonResponse
|
||||
{
|
||||
if ($notification->user_id !== $request->user()->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$notification->markAsRead();
|
||||
$this->notificationService->markAsRead($notification, $request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function markAllAsRead(Request $request): JsonResponse
|
||||
{
|
||||
Notification::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->unread()
|
||||
->update(['is_read' => true, 'read_at' => now()]);
|
||||
$this->notificationService->markAllAsRead($request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Notification $notification): JsonResponse
|
||||
{
|
||||
if ($notification->user_id !== $request->user()->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$notification->delete();
|
||||
$this->notificationService->delete($notification, $request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
public function destroyAll(Request $request): JsonResponse
|
||||
{
|
||||
Notification::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->delete();
|
||||
$this->notificationService->deleteAll($request->user());
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\System;
|
||||
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -34,17 +33,8 @@ public function index(Request $request): Response
|
||||
|
||||
public function create(): Response
|
||||
{
|
||||
$permissions = collect(PermissionEnum::cases())
|
||||
->map(fn (PermissionEnum $permission) => [
|
||||
'value' => $permission->value,
|
||||
'label' => $permission->label(),
|
||||
'group' => $permission->group(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return Inertia::render('admin/system/roles/Create', [
|
||||
'permissions' => $permissions,
|
||||
'permissions' => $this->roleService->permissionOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -59,24 +49,9 @@ public function store(RoleRequest $request): RedirectResponse
|
||||
|
||||
public function edit(Role $role): Response
|
||||
{
|
||||
$role->load('permissions');
|
||||
|
||||
$permissions = collect(PermissionEnum::cases())
|
||||
->map(fn (PermissionEnum $permission) => [
|
||||
'value' => $permission->value,
|
||||
'label' => $permission->label(),
|
||||
'group' => $permission->group(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return Inertia::render('admin/system/roles/Edit', [
|
||||
'role' => [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'permissions' => $role->permissions->pluck('name'),
|
||||
],
|
||||
'permissions' => $permissions,
|
||||
...$this->roleService->findForEdit($role),
|
||||
'permissions' => $this->roleService->permissionOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -10,13 +10,12 @@
|
||||
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
|
||||
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
|
||||
use App\Http\Requests\Admin\System\Setting\SystemRequest;
|
||||
use App\Models\OwnerVerificationRequest;
|
||||
use App\Services\Manage\OwnerVerificationService;
|
||||
use App\Services\System\Setting\HomepageSettingService;
|
||||
use App\Services\System\Setting\HrSettingService;
|
||||
use App\Services\System\Setting\MarketplaceService;
|
||||
use App\Services\System\Setting\SocialMediaService;
|
||||
use App\Services\System\Setting\SystemService;
|
||||
use App\Settings\MarketplaceSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -31,6 +30,7 @@ public function __construct(
|
||||
private readonly MarketplaceService $marketplaceService,
|
||||
private readonly HrSettingService $hrSettingService,
|
||||
private readonly HomepageSettingService $homepageSettingService,
|
||||
private readonly OwnerVerificationService $ownerVerificationService,
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
@ -41,10 +41,7 @@ public function index(): Response
|
||||
'marketplace' => $this->marketplaceService->marketplaceData(),
|
||||
'hr' => $this->hrSettingService->hrData(),
|
||||
'homepage' => $this->homepageSettingService->homepageData(),
|
||||
'hasPendingMarketplaceVerification' => OwnerVerificationRequest::query()
|
||||
->where('subject_type', MarketplaceSettings::class)
|
||||
->pending()
|
||||
->exists(),
|
||||
'hasPendingMarketplaceVerification' => $this->ownerVerificationService->hasPendingMarketplaceVerification(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -78,12 +75,7 @@ public function updateMarketplace(MarketplaceRequest $request): RedirectResponse
|
||||
{
|
||||
$this->marketplaceService->updateMarketplace($request->validated(), $request->user());
|
||||
|
||||
$hasPending = OwnerVerificationRequest::query()
|
||||
->where('subject_type', MarketplaceSettings::class)
|
||||
->pending()
|
||||
->exists();
|
||||
|
||||
if ($hasPending) {
|
||||
if ($this->ownerVerificationService->hasPendingMarketplaceVerification()) {
|
||||
$this->flashSuccess('Perubahan pengaturan marketplace berhasil diajukan dan menunggu verifikasi owner.');
|
||||
} else {
|
||||
$this->flashSuccess('Pengaturan marketplace berhasil disimpan.');
|
||||
|
||||
@ -6,9 +6,6 @@
|
||||
|
||||
trait ParsesDataTableQuery
|
||||
{
|
||||
/**
|
||||
* @return array{search: string, sort: string, direction: 'asc'|'desc'}
|
||||
*/
|
||||
protected function parseDataTableQuery(Request $request): array
|
||||
{
|
||||
return [
|
||||
@ -18,10 +15,6 @@ protected function parseDataTableQuery(Request $request): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $query
|
||||
* @return array{search: string, sort: string, direction: 'asc'|'desc'|null}
|
||||
*/
|
||||
protected function dataTableFilters(array $query, array $extra = []): array
|
||||
{
|
||||
return array_merge([
|
||||
|
||||
@ -2,76 +2,18 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use App\Models\SystemConfiguration;
|
||||
use App\Services\Manage\CuttingResultPriceResolver;
|
||||
use App\Services\System\Setting\HomepageSettingService;
|
||||
use App\Settings\SocialMediaSettings;
|
||||
use App\Settings\SystemSettings;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use App\Services\System\HomepageService;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
|
||||
private readonly HomepageSettingService $homepageSettingService,
|
||||
private readonly HomepageService $homepageService,
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$categories = Category::getActiveWithProducts();
|
||||
|
||||
$products = Product::getActiveWithVariantsAndCategories();
|
||||
|
||||
$allVariantIds = $products
|
||||
->flatMap(fn ($product) => $product->variants->pluck('id'))
|
||||
->all();
|
||||
|
||||
$allPricesByVariant = $this->cuttingResultPriceResolver->latestPricesForVariants($allVariantIds);
|
||||
|
||||
$products = $products->map(function ($product) use ($allPricesByVariant) {
|
||||
$product->variants->each(function ($variant) use ($allPricesByVariant) {
|
||||
$variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
|
||||
$variantPrices = $allPricesByVariant->get($variant->id, collect());
|
||||
$variant->setAttribute(
|
||||
'prices',
|
||||
$variantPrices->map(fn ($price) => [
|
||||
'type' => $price->price_type->value,
|
||||
'type_label' => $price->price_type->label(),
|
||||
'price' => (int) $price->price,
|
||||
'price_formatted' => $price->price_formatted,
|
||||
])->values()->all(),
|
||||
);
|
||||
});
|
||||
|
||||
return $product;
|
||||
});
|
||||
|
||||
$configuration = SystemConfiguration::instance();
|
||||
$logo = MediaPresenter::first($configuration, 'logo');
|
||||
$logoUrl = $logo['url'] ?? null;
|
||||
|
||||
$settings = app(SystemSettings::class);
|
||||
$socialSettings = app(SocialMediaSettings::class);
|
||||
|
||||
$appName = $settings->app_name ?? 'DST Collection';
|
||||
|
||||
return Inertia::render('Home', [
|
||||
'categories' => $categories,
|
||||
'products' => $products,
|
||||
'appName' => $appName,
|
||||
'aboutApp' => $settings->about_app ?? '',
|
||||
'contactEmail' => $settings->email ?? '',
|
||||
'contactPhone' => $settings->phone ?? '',
|
||||
'contactAddress' => $settings->address ?? '',
|
||||
'logoUrl' => $logoUrl,
|
||||
'instagramUrl' => $socialSettings->instagram_url ?? null,
|
||||
'facebookUrl' => $socialSettings->facebook_url ?? null,
|
||||
'tiktokUrl' => $socialSettings->tiktok_url ?? null,
|
||||
'homepage' => $this->homepageSettingService->homepageData(),
|
||||
]);
|
||||
return Inertia::render('Home', $this->homepageService->pageData());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
}
|
||||
@ -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 [];
|
||||
}
|
||||
}
|
||||
@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php
Normal file
19
app/Http/Requests/Admin/Hr/ApproveLeaveRequestRequest.php
Normal 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 [];
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StockEcerTransferRequest extends FormRequest
|
||||
class StockRetailTransferRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
32
app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php
Normal file
32
app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php
Normal 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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@ public function rules(): array
|
||||
],
|
||||
'variants.*.name' => ['required', 'string', 'max:200'],
|
||||
'variants.*.stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.stock_ecer' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.stock_retail' => ['required', 'integer', 'min:0'],
|
||||
...$this->variantImageRules(),
|
||||
];
|
||||
}
|
||||
@ -60,7 +60,7 @@ public function attributes(): array
|
||||
'variants' => 'varian',
|
||||
'variants.*.name' => 'nama varian',
|
||||
'variants.*.stock' => 'stok',
|
||||
'variants.*.stock_ecer' => 'stok ecer',
|
||||
'variants.*.stock_retail' => 'stok ecer',
|
||||
...$this->variantImageAttributes('variants', 'foto varian'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -127,9 +127,9 @@ protected function distributor(Builder $query): void
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function ecer(Builder $query): void
|
||||
protected function retail(Builder $query): void
|
||||
{
|
||||
$query->where('price_type', PriceType::ECER);
|
||||
$query->where('price_type', PriceType::RETAIL);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
|
||||
@ -26,7 +26,7 @@ protected function casts(): array
|
||||
return [
|
||||
'reject_stock' => 'integer',
|
||||
'stock' => 'integer',
|
||||
'stock_ecer' => 'integer',
|
||||
'stock_retail' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class StockEcerHistory extends Model
|
||||
class StockRetailHistory extends Model
|
||||
{
|
||||
protected $table = 'stock_ecer_histories';
|
||||
protected $table = 'stock_retail_histories';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
@ -21,8 +21,8 @@ protected function casts(): array
|
||||
'quantity' => 'integer',
|
||||
'stock_after' => 'integer',
|
||||
'stock_before' => 'integer',
|
||||
'stock_ecer_after' => 'integer',
|
||||
'stock_ecer_before' => 'integer',
|
||||
'stock_retail_after' => 'integer',
|
||||
'stock_retail_before' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Finance;
|
||||
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\Role;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Models\EmployeeAdvancePayment;
|
||||
@ -309,4 +310,21 @@ private function applySorting(Builder $query, string $sort, string $direction):
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
public function canSubmit(User $user): bool
|
||||
{
|
||||
return $user->can(Permission::EMPLOYEE_ADVANCES_CREATE->value)
|
||||
&& ! $user->can(Permission::EMPLOYEE_ADVANCES_VERIFY->value)
|
||||
&& $user->employee !== null;
|
||||
}
|
||||
|
||||
public function indexPageData(array $tableQuery, User $user, string $status = ''): array
|
||||
{
|
||||
return [
|
||||
'employeeAdvances' => $this->paginateForIndex($tableQuery, $user, $status),
|
||||
'summary' => $this->outstandingSummary($user),
|
||||
'authEmployeeId' => $user->employee?->id,
|
||||
'canSubmit' => $this->canSubmit($user),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Services\Hr;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Employee;
|
||||
use App\Models\LeaveRequest;
|
||||
@ -175,4 +177,45 @@ public function delete(Attendance $attendance): void
|
||||
route('admin.hr.attendances.index'),
|
||||
);
|
||||
}
|
||||
|
||||
public function resolveAttendanceScope(User $user): array
|
||||
{
|
||||
$employee = $user->employee;
|
||||
$isManager = $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]);
|
||||
|
||||
return [
|
||||
'scopedEmployeeId' => $isManager ? null : $employee?->id,
|
||||
'hasScopedAccess' => $isManager || $employee !== null,
|
||||
'isManager' => $isManager,
|
||||
'employee' => $employee,
|
||||
'canCheckIn' => $user->can(Permission::ATTENDANCES_CREATE->value) && $employee !== null,
|
||||
];
|
||||
}
|
||||
|
||||
public function indexPageData(User $user, CarbonInterface $start, CarbonInterface $end): array
|
||||
{
|
||||
$scope = $this->resolveAttendanceScope($user);
|
||||
$employee = $scope['employee'];
|
||||
|
||||
return [
|
||||
'attendances' => $this->listForCalendar(
|
||||
$start,
|
||||
$end,
|
||||
$scope['scopedEmployeeId'],
|
||||
$scope['hasScopedAccess'],
|
||||
),
|
||||
'todayAttendance' => $employee
|
||||
? $this->todayAttendanceForEmployee($employee)
|
||||
: null,
|
||||
'isOnLeave' => $employee
|
||||
? $this->isOnLeaveToday($employee)
|
||||
: false,
|
||||
'canCheckIn' => $scope['canCheckIn'],
|
||||
'canManageAll' => $scope['isManager'],
|
||||
'calendarRange' => [
|
||||
'start' => $start->toDateString(),
|
||||
'end' => $end->toDateString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\User;
|
||||
use App\Models\UserProfile;
|
||||
use App\Services\Media\MediaService;
|
||||
use App\Support\Media\MediaPresenter;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@ -58,6 +59,23 @@ public function paginateForIndex(
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
public function assignableRoleOptions(): array
|
||||
{
|
||||
return Role::assignableSelectOptions();
|
||||
}
|
||||
|
||||
public function findForEdit(User $user): array
|
||||
{
|
||||
$user->load(['profile', 'employee', 'roles']);
|
||||
|
||||
return [
|
||||
'employee' => $user,
|
||||
'profilePhoto' => $user->profile
|
||||
? MediaPresenter::first($user->profile, 'profile_photo')
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Hr;
|
||||
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Enums\Permission;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Models\User;
|
||||
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
||||
@ -224,4 +225,33 @@ private function applySorting(Builder $query, string $sort, string $direction):
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
|
||||
public function hasPendingForEmployee(User $user): bool
|
||||
{
|
||||
if ($user->employee === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LeaveRequest::query()
|
||||
->pending()
|
||||
->where('employee_id', $user->employee->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canSubmit(User $user): bool
|
||||
{
|
||||
return $user->can(Permission::LEAVE_REQUESTS_CREATE->value)
|
||||
&& ! $user->can(Permission::LEAVE_REQUESTS_VERIFY->value)
|
||||
&& $user->employee !== null;
|
||||
}
|
||||
|
||||
public function indexPageData(array $tableQuery, User $user): array
|
||||
{
|
||||
return [
|
||||
'leaveRequests' => $this->paginateForIndex($tableQuery, $user),
|
||||
'authEmployeeId' => $user->employee?->id,
|
||||
'canSubmit' => $this->canSubmit($user),
|
||||
'hasPending' => $this->hasPendingForEmployee($user),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,26 @@ public function isEditable(CuttingStatus $status): bool
|
||||
return $status->isEditable();
|
||||
}
|
||||
|
||||
public function ensureEditable(Cutting $cutting): void
|
||||
{
|
||||
if (! $this->isEditable($cutting->status)) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => 'Proses cutting tidak dapat diubah.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function transitionStatusMessage(CuttingStatus $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
|
||||
CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
|
||||
CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
|
||||
CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
|
||||
default => 'Status proses cutting berhasil diperbarui.',
|
||||
};
|
||||
}
|
||||
|
||||
public function canTransitionTo(CuttingStatus $from, CuttingStatus $to): bool
|
||||
{
|
||||
return $from->canTransitionTo($to);
|
||||
|
||||
@ -44,6 +44,25 @@ public function isEditable(OrderStatus $status): bool
|
||||
return $status->isEditable();
|
||||
}
|
||||
|
||||
public function ensureEditable(Order $order): void
|
||||
{
|
||||
if (! $this->isEditable($order->status)) {
|
||||
throw ValidationException::withMessages([
|
||||
'status' => 'Pesanan tidak dapat diubah.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function transitionStatusMessage(OrderStatus $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
|
||||
OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
|
||||
OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
|
||||
default => 'Status pesanan berhasil diperbarui.',
|
||||
};
|
||||
}
|
||||
|
||||
public function canTransitionTo(OrderStatus $from, OrderStatus $to): bool
|
||||
{
|
||||
return $from->canTransitionTo($to);
|
||||
@ -76,7 +95,7 @@ public function stockColumn(ProductStockQuality $quality): string
|
||||
return match ($quality) {
|
||||
ProductStockQuality::GOOD => 'stock',
|
||||
ProductStockQuality::REJECT => 'reject_stock',
|
||||
ProductStockQuality::ECER => 'stock_ecer',
|
||||
ProductStockQuality::RETAIL => 'stock_retail',
|
||||
};
|
||||
}
|
||||
|
||||
@ -331,13 +350,13 @@ public function draftItemsForUser(User $user): array
|
||||
*/
|
||||
public function syncDraftItem(array $validated, User $user): array
|
||||
{
|
||||
// Force ecer price type and stock quality for cashier role
|
||||
// Force retail price type and stock quality for cashier role
|
||||
$priceType = $user->hasRole('cashier')
|
||||
? PriceType::ECER
|
||||
? PriceType::RETAIL
|
||||
: PriceType::from($validated['price_type']);
|
||||
|
||||
$stockQuality = $user->hasRole('cashier')
|
||||
? ProductStockQuality::ECER
|
||||
? ProductStockQuality::RETAIL
|
||||
: ProductStockQuality::from($validated['stock_quality']);
|
||||
|
||||
$variant = ProductVariant::query()->findOrFail($validated['product_variant_id']);
|
||||
@ -391,9 +410,9 @@ public function removeDraftItem(User $user, ProductVariant $productVariant, Prod
|
||||
*/
|
||||
public function resyncDraftPrices(User $user, string $priceTypeValue): array
|
||||
{
|
||||
// Force ecer price type for cashier role
|
||||
// Force retail price type for cashier role
|
||||
$priceType = $user->hasRole('cashier')
|
||||
? PriceType::ECER
|
||||
? PriceType::RETAIL
|
||||
: PriceType::from($priceTypeValue);
|
||||
|
||||
$items = $this->draftItemsQuery($user)
|
||||
@ -430,7 +449,7 @@ public function create(array $validated, User $user): Order
|
||||
// Force cashier settings
|
||||
if ($user->hasRole('cashier')) {
|
||||
$validated['channel'] = 'store';
|
||||
$validated['price_type'] = 'ecer';
|
||||
$validated['price_type'] = 'retail';
|
||||
$validated['payment_type'] = 'cash';
|
||||
unset($validated['customer_id']);
|
||||
}
|
||||
|
||||
@ -36,9 +36,17 @@ public function __construct(
|
||||
private readonly PurchaseService $purchaseService,
|
||||
private readonly PushNotificationService $pushNotificationService,
|
||||
private readonly MarketplaceService $marketplaceService,
|
||||
private readonly StockEcerService $stockEcerService,
|
||||
private readonly StockRetailService $stockRetailService,
|
||||
) {}
|
||||
|
||||
public function hasPendingMarketplaceVerification(): bool
|
||||
{
|
||||
return OwnerVerificationRequest::query()
|
||||
->where('subject_type', MarketplaceSettings::class)
|
||||
->pending()
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
@ -284,7 +292,7 @@ private function rejectVerificationRequest(OwnerVerificationRequest $request): v
|
||||
RawMaterial::class => $this->rawMaterialService->rejectVerificationRequest($request),
|
||||
Purchase::class => $this->purchaseService->rejectVerificationRequest($request),
|
||||
MarketplaceSettings::class => null,
|
||||
ProductVariant::class => $this->stockEcerService->rejectStockEcerTransfer($request),
|
||||
ProductVariant::class => $this->stockRetailService->rejectStockRetailTransfer($request),
|
||||
default => throw ValidationException::withMessages([
|
||||
'subject_type' => 'Tipe data verifikasi tidak didukung.',
|
||||
]),
|
||||
@ -298,7 +306,7 @@ private function applyVerificationRequest(OwnerVerificationRequest $request): vo
|
||||
RawMaterial::class => $this->rawMaterialService->applyVerificationRequest($request),
|
||||
Purchase::class => $this->purchaseService->applyVerificationRequest($request),
|
||||
MarketplaceSettings::class => $this->marketplaceService->applyVerificationRequest($request),
|
||||
ProductVariant::class => $this->stockEcerService->applyStockEcerTransfer($request),
|
||||
ProductVariant::class => $this->stockRetailService->applyStockRetailTransfer($request),
|
||||
default => throw ValidationException::withMessages([
|
||||
'subject_type' => 'Tipe data verifikasi tidak didukung.',
|
||||
]),
|
||||
|
||||
@ -6,21 +6,21 @@
|
||||
use App\Enums\OwnerVerificationStatus;
|
||||
use App\Models\OwnerVerificationRequest;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StockEcerHistory;
|
||||
use App\Models\StockRetailHistory;
|
||||
use App\Models\User;
|
||||
use App\Services\System\PushNotificationService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class StockEcerService
|
||||
class StockRetailService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PushNotificationService $pushNotificationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Submit a stock ecer transfer request for owner verification.
|
||||
* Submit a stock retail transfer request for owner verification.
|
||||
*/
|
||||
public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): OwnerVerificationRequest
|
||||
{
|
||||
@ -41,7 +41,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
$existingPending = OwnerVerificationRequest::query()
|
||||
->where('subject_type', ProductVariant::class)
|
||||
->where('subject_id', $variant->id)
|
||||
->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER)
|
||||
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
|
||||
->where('status', OwnerVerificationStatus::PENDING)
|
||||
->exists();
|
||||
|
||||
@ -53,7 +53,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
|
||||
try {
|
||||
$request = OwnerVerificationRequest::create([
|
||||
'action' => OwnerVerificationAction::STOCK_ECER_TRANSFER,
|
||||
'action' => OwnerVerificationAction::STOCK_RETAIL_TRANSFER,
|
||||
'status' => OwnerVerificationStatus::PENDING,
|
||||
'subject_type' => ProductVariant::class,
|
||||
'subject_id' => $variant->id,
|
||||
@ -61,12 +61,12 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
'payload' => [
|
||||
'old' => [
|
||||
'stock' => $variant->stock,
|
||||
'stock_ecer' => $variant->stock_ecer,
|
||||
'stock_retail' => $variant->stock_retail,
|
||||
],
|
||||
'new' => [
|
||||
'quantity' => $quantity,
|
||||
'stock' => $variant->stock - $quantity,
|
||||
'stock_ecer' => $variant->stock_ecer + $quantity,
|
||||
'stock_retail' => $variant->stock_retail + $quantity,
|
||||
'notes' => $notes,
|
||||
'variant_name' => $variant->name,
|
||||
'product_name' => $variant->product?->name ?? '-',
|
||||
@ -103,9 +103,9 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the stock ecer transfer when owner approves.
|
||||
* Apply the stock retail transfer when owner approves.
|
||||
*/
|
||||
public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
|
||||
public function applyStockRetailTransfer(OwnerVerificationRequest $request): void
|
||||
{
|
||||
$payload = $request->payload ?? [];
|
||||
$newData = $payload['new'] ?? [];
|
||||
@ -122,19 +122,19 @@ public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
|
||||
->firstOrFail();
|
||||
|
||||
$stockBefore = $variant->stock;
|
||||
$stockEcerBefore = $variant->stock_ecer;
|
||||
$stockRetailBefore = $variant->stock_retail;
|
||||
|
||||
$variant->decrement('stock', $quantity);
|
||||
$variant->increment('stock_ecer', $quantity);
|
||||
$variant->increment('stock_retail', $quantity);
|
||||
|
||||
StockEcerHistory::create([
|
||||
StockRetailHistory::create([
|
||||
'product_variant_id' => $variant->id,
|
||||
'user_id' => $request->submitted_by_id,
|
||||
'quantity' => $quantity,
|
||||
'stock_before' => $stockBefore,
|
||||
'stock_ecer_before' => $stockEcerBefore,
|
||||
'stock_retail_before' => $stockRetailBefore,
|
||||
'stock_after' => $stockBefore - $quantity,
|
||||
'stock_ecer_after' => $stockEcerBefore + $quantity,
|
||||
'stock_retail_after' => $stockRetailBefore + $quantity,
|
||||
'notes' => $newData['notes'] ?? null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
@ -142,9 +142,9 @@ public function applyStockEcerTransfer(OwnerVerificationRequest $request): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the stock ecer transfer (no-op since nothing changed).
|
||||
* Reject the stock retail transfer (no-op since nothing changed).
|
||||
*/
|
||||
public function rejectStockEcerTransfer(OwnerVerificationRequest $request): void
|
||||
public function rejectStockRetailTransfer(OwnerVerificationRequest $request): void
|
||||
{
|
||||
// No-op: nothing was changed yet, so nothing to rollback.
|
||||
}
|
||||
@ -32,6 +32,11 @@ public function create(array $validated): void
|
||||
Customer::create($validated);
|
||||
}
|
||||
|
||||
public function createAndReturn(array $validated): Customer
|
||||
{
|
||||
return Customer::create($validated);
|
||||
}
|
||||
|
||||
public function update(Customer $customer, array $validated): void
|
||||
{
|
||||
$customer->update($validated);
|
||||
|
||||
@ -76,12 +76,12 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
||||
|
||||
$pendingRequest = $product->pendingOwnerVerificationRequest;
|
||||
|
||||
// Also check for pending stock ecer transfer on variants
|
||||
// Also check for pending stock retail transfer on variants
|
||||
if ($pendingRequest === null) {
|
||||
$pendingRequest = OwnerVerificationRequest::query()
|
||||
->where('subject_type', ProductVariant::class)
|
||||
->whereIn('subject_id', $product->variants->pluck('id'))
|
||||
->where('action', OwnerVerificationAction::STOCK_ECER_TRANSFER)
|
||||
->where('action', OwnerVerificationAction::STOCK_RETAIL_TRANSFER)
|
||||
->pending()
|
||||
->latest()
|
||||
->first();
|
||||
@ -100,6 +100,22 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
||||
});
|
||||
}
|
||||
|
||||
public function findForEdit(Product $product): Product
|
||||
{
|
||||
$product->load([
|
||||
'categories',
|
||||
'variants' => fn ($query) => $query
|
||||
->with('media')
|
||||
->orderBy('created_at'),
|
||||
]);
|
||||
|
||||
$product->variants->each(function (ProductVariant $variant): void {
|
||||
$variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
|
||||
});
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
@ -119,7 +135,7 @@ public function create(array $validated, User $user): void
|
||||
$variant = $product->variants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_ecer' => $variantData['stock_ecer'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
]);
|
||||
|
||||
$this->syncVariantImages($variant, $variantData, $index);
|
||||
@ -381,7 +397,7 @@ private function applyPayloadToProduct(
|
||||
$variant->update([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_ecer' => $variantData['stock_ecer'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
]);
|
||||
|
||||
if ($verificationRequest !== null) {
|
||||
@ -394,7 +410,7 @@ private function applyPayloadToProduct(
|
||||
$variant = $product->variants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_ecer' => $variantData['stock_ecer'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
]);
|
||||
|
||||
if ($verificationRequest !== null) {
|
||||
@ -539,7 +555,7 @@ private function snapshotProduct(Product $product): array
|
||||
'id' => $variant->id,
|
||||
'name' => $variant->name,
|
||||
'stock' => $variant->stock,
|
||||
'stock_ecer' => $variant->stock_ecer,
|
||||
'stock_retail' => $variant->stock_retail,
|
||||
])
|
||||
->all(),
|
||||
]);
|
||||
@ -575,7 +591,7 @@ private function buildPayloadFromValidated(array $validated): array
|
||||
'id' => $variantData['id'] ?? null,
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'stock_ecer' => $variantData['stock_ecer'],
|
||||
'stock_retail' => $variantData['stock_retail'],
|
||||
'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
|
||||
])
|
||||
->all(),
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\Role;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
@ -498,7 +499,7 @@ public function getProductStock(): array
|
||||
->selectRaw('
|
||||
SUM(product_variants.stock) as total_stock,
|
||||
SUM(product_variants.reject_stock) as total_reject,
|
||||
SUM(product_variants.stock_ecer) as total_ecer,
|
||||
SUM(product_variants.stock_retail) as total_retail,
|
||||
COUNT(product_variants.id) as total_variants,
|
||||
COUNT(DISTINCT products.id) as total_products
|
||||
')
|
||||
@ -516,7 +517,7 @@ public function getProductStock(): array
|
||||
return [
|
||||
'total_stock' => (int) ($variants->total_stock ?? 0),
|
||||
'total_reject' => (int) ($variants->total_reject ?? 0),
|
||||
'total_ecer' => (int) ($variants->total_ecer ?? 0),
|
||||
'total_retail' => (int) ($variants->total_retail ?? 0),
|
||||
'total_value' => (int) ($totalValue ?? 0),
|
||||
'total_products' => (int) ($variants->total_products ?? 0),
|
||||
'total_variants' => (int) ($variants->total_variants ?? 0),
|
||||
@ -577,4 +578,9 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function isManager(?User $user): bool
|
||||
{
|
||||
return $user?->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
77
app/Services/System/HomepageService.php
Normal file
77
app/Services/System/HomepageService.php
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
69
app/Services/System/NotificationService.php
Normal file
69
app/Services/System/NotificationService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Enums\Role as EnumsRole;
|
||||
use App\Enums\Permission as PermissionEnum;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@ -31,6 +32,31 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
public function permissionOptions(): array
|
||||
{
|
||||
return collect(PermissionEnum::cases())
|
||||
->map(fn (PermissionEnum $permission) => [
|
||||
'value' => $permission->value,
|
||||
'label' => $permission->label(),
|
||||
'group' => $permission->group(),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function findForEdit(Role $role): array
|
||||
{
|
||||
$role->load('permissions');
|
||||
|
||||
return [
|
||||
'role' => [
|
||||
'id' => $role->id,
|
||||
'name' => $role->name,
|
||||
'permissions' => $role->permissions->pluck('name'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function create(array $validated): void
|
||||
{
|
||||
try {
|
||||
|
||||
@ -77,7 +77,7 @@ private static function label(string $field): string
|
||||
'prices' => 'Varian Harga',
|
||||
'quantity' => 'Jumlah Transfer',
|
||||
'stock' => 'Stok Bagus',
|
||||
'stock_ecer' => 'Stok Ecer',
|
||||
'stock_retail' => 'Stok Ecer',
|
||||
'variant_name' => 'Varian',
|
||||
'product_name' => 'Produk',
|
||||
'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform',
|
||||
@ -186,7 +186,7 @@ private static function presentValue(string $field, mixed $value): mixed
|
||||
return implode(', ', $value);
|
||||
}
|
||||
|
||||
if (in_array($field, ['quantity', 'stock', 'stock_ecer'], true)) {
|
||||
if (in_array($field, ['quantity', 'stock', 'stock_retail'], true)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ public function up(): void
|
||||
$table->string('name', 200);
|
||||
$table->unsignedInteger('stock')->default(0);
|
||||
$table->unsignedInteger('reject_stock')->default(0);
|
||||
$table->unsignedInteger('stock_ecer')->default(0);
|
||||
$table->unsignedInteger('stock_retail')->default(0);
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stock_ecer_histories', function (Blueprint $table) {
|
||||
Schema::create('stock_retail_histories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('product_variant_id')->constrained('product_variants')->cascadeOnDelete();
|
||||
@ -16,9 +16,9 @@ public function up(): void
|
||||
|
||||
$table->unsignedInteger('quantity');
|
||||
$table->unsignedInteger('stock_before');
|
||||
$table->unsignedInteger('stock_ecer_before');
|
||||
$table->unsignedInteger('stock_retail_before');
|
||||
$table->unsignedInteger('stock_after');
|
||||
$table->unsignedInteger('stock_ecer_after');
|
||||
$table->unsignedInteger('stock_retail_after');
|
||||
$table->string('notes')->nullable();
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
@ -27,6 +27,6 @@ public function up(): void
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stock_ecer_histories');
|
||||
Schema::dropIfExists('stock_retail_histories');
|
||||
}
|
||||
};
|
||||
@ -52,7 +52,7 @@ const getProductImage = (product: Product, indexOffset = 0) => {
|
||||
|
||||
const getProductPriceRange = (product: Product) => {
|
||||
const prices = product.variants.flatMap(v =>
|
||||
v.prices.filter(p => p.type === 'ecer').map(p => p.price)
|
||||
v.prices.filter(p => p.type === 'retail').map(p => p.price)
|
||||
);
|
||||
|
||||
if (prices.length === 0) {
|
||||
|
||||
@ -14,7 +14,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const activeVariant = ref<Variant | null>(null);
|
||||
const activePriceType = ref<string>('ecer');
|
||||
const activePriceType = ref<string>('retail');
|
||||
const activeImageIndex = ref(0);
|
||||
|
||||
const allImages = computed<MediaItem[]>(() => {
|
||||
@ -40,7 +40,7 @@ const mainImage = computed(() => {
|
||||
watch(() => props.product, (newProduct) => {
|
||||
if (newProduct) {
|
||||
activeVariant.value = newProduct.variants[0] || null;
|
||||
activePriceType.value = 'ecer';
|
||||
activePriceType.value = 'retail';
|
||||
activeImageIndex.value = 0;
|
||||
} else {
|
||||
activeVariant.value = null;
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
} from '@/components/ui/field';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { transfer } from '@/routes/admin/manage/stock-ecer';
|
||||
import { transfer } from '@/routes/admin/manage/stock-retail';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
@ -29,7 +29,7 @@ const props = defineProps<{
|
||||
variantName: string;
|
||||
productName: string;
|
||||
stockBagus: number;
|
||||
stockEcer: number;
|
||||
stockRetail: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -116,7 +116,7 @@ function formatNumber(value: number): string {
|
||||
</div>
|
||||
<div class="flex justify-between text-xs">
|
||||
<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>
|
||||
|
||||
@ -128,9 +128,9 @@ function formatNumber(value: number): string {
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="ecer-quantity" required>Jumlah Transfer</FieldLabel>
|
||||
<FieldLabel for="retail-quantity" required>Jumlah Transfer</FieldLabel>
|
||||
<NumberInput
|
||||
id="ecer-quantity"
|
||||
id="retail-quantity"
|
||||
v-model="form.quantity"
|
||||
:max="stockBagus"
|
||||
placeholder="Masukkan jumlah"
|
||||
@ -145,9 +145,9 @@ function formatNumber(value: number): string {
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="ecer-notes">Catatan</FieldLabel>
|
||||
<FieldLabel for="retail-notes">Catatan</FieldLabel>
|
||||
<Textarea
|
||||
id="ecer-notes"
|
||||
id="retail-notes"
|
||||
v-model="form.notes"
|
||||
rows="2"
|
||||
placeholder="Contoh: Transfer untuk kebutuhan kasir"
|
||||
@ -1,5 +1,5 @@
|
||||
export const StockQuality = {
|
||||
GOOD: 'good',
|
||||
REJECT: 'reject',
|
||||
ECER: 'ecer',
|
||||
RETAIL: 'retail',
|
||||
} as const;
|
||||
|
||||
@ -129,7 +129,7 @@ const getProductImage = (product: Product, indexOffset = 0) => {
|
||||
// Price Range Resolver
|
||||
const getProductPriceRange = (product: Product) => {
|
||||
const prices = product.variants.flatMap(v =>
|
||||
v.prices.filter(p => p.type === 'ecer').map(p => p.price)
|
||||
v.prices.filter(p => p.type === 'retail').map(p => p.price)
|
||||
);
|
||||
|
||||
if (prices.length === 0) {
|
||||
|
||||
@ -109,7 +109,7 @@ const isCashierUser = computed(() =>
|
||||
);
|
||||
|
||||
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 customerFormOpen = ref(false);
|
||||
const cartDetailOpen = ref(false);
|
||||
@ -131,7 +131,7 @@ const form = useForm({
|
||||
customer_id: '',
|
||||
marketing_id: defaultMarketingId.value || 'none',
|
||||
channel: 'store',
|
||||
price_type: 'ecer',
|
||||
price_type: 'retail',
|
||||
payment_type: OrderPaymentType.CASH,
|
||||
is_affiliate: false,
|
||||
tiktok_order_id: '',
|
||||
@ -200,7 +200,7 @@ watch(
|
||||
form.price_type = 'tiktok';
|
||||
form.payment_type = OrderPaymentType.MARKETPLACE;
|
||||
} else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) {
|
||||
form.price_type = 'ecer';
|
||||
form.price_type = 'retail';
|
||||
}
|
||||
|
||||
if (channel === 'store') {
|
||||
@ -250,7 +250,7 @@ function upsertCartItem(item: OrderCartItem) {
|
||||
|
||||
function availableStockForVariant(variant: ProductVariantItem, stockQuality: string): number {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -337,7 +337,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
|
||||
const availableStock = availableStockForVariant(variant, stockQuality);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -350,7 +350,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
|
||||
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -467,10 +467,10 @@ function buildFormData(): FormData {
|
||||
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) {
|
||||
formData.append('channel', 'store');
|
||||
formData.append('price_type', 'ecer');
|
||||
formData.append('price_type', 'retail');
|
||||
formData.append('payment_type', OrderPaymentType.CASH);
|
||||
} else {
|
||||
formData.append('channel', form.channel);
|
||||
@ -596,7 +596,7 @@ function submit() {
|
||||
{{ variant.name }}
|
||||
</p>
|
||||
<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>
|
||||
<span class="tabular-nums">Bagus: {{ variant.stock }}</span>
|
||||
<span class="mx-1">·</span>
|
||||
@ -813,7 +813,7 @@ function submit() {
|
||||
·
|
||||
{{ item.stock_quality_label ?? (item.stock_quality ===
|
||||
StockQuality.REJECT
|
||||
? 'Reject' : item.stock_quality === StockQuality.ECER
|
||||
? 'Reject' : item.stock_quality === StockQuality.RETAIL
|
||||
? 'Eceran' : 'Bagus') }}
|
||||
</p>
|
||||
</div>
|
||||
@ -951,7 +951,7 @@ function submit() {
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
{{ item.variant_name }}
|
||||
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject'
|
||||
: item.stock_quality === StockQuality.ECER ? 'Eceran' : 'Bagus')
|
||||
: item.stock_quality === StockQuality.RETAIL ? 'Eceran' : 'Bagus')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -21,7 +21,7 @@ const initialData = computed(() => ({
|
||||
id: variant.id,
|
||||
name: variant.name,
|
||||
stock: variant.stock,
|
||||
stock_ecer: variant.stock_ecer,
|
||||
stock_retail: variant.stock_retail,
|
||||
images: variant.images ?? [],
|
||||
})),
|
||||
}));
|
||||
|
||||
@ -33,7 +33,7 @@ const props = withDefaults(
|
||||
id?: number;
|
||||
name?: string;
|
||||
stock?: number | string;
|
||||
stock_ecer?: number | string;
|
||||
stock_retail?: number | string;
|
||||
images?: Array<{ id: number; url: string; thumb_url: string }>;
|
||||
}>;
|
||||
};
|
||||
@ -64,7 +64,7 @@ const {
|
||||
client_id: createClientId(),
|
||||
name: '',
|
||||
stock: '0',
|
||||
stock_ecer: '0',
|
||||
stock_retail: '0',
|
||||
media: createMediaUploadState(),
|
||||
}),
|
||||
() => {
|
||||
@ -73,7 +73,7 @@ const {
|
||||
client_id: createClientId(),
|
||||
name: '',
|
||||
stock: '0',
|
||||
stock_ecer: '0',
|
||||
stock_retail: '0',
|
||||
media: createMediaUploadState(),
|
||||
}];
|
||||
}
|
||||
@ -83,7 +83,7 @@ const {
|
||||
id: variant.id,
|
||||
name: variant.name ?? '',
|
||||
stock: variant.stock != null ? String(variant.stock) : '0',
|
||||
stock_ecer: variant.stock_ecer != null ? String(variant.stock_ecer) : '0',
|
||||
stock_retail: variant.stock_retail != null ? String(variant.stock_retail) : '0',
|
||||
media: createMediaUploadState(variant.images ?? []),
|
||||
}));
|
||||
},
|
||||
@ -126,7 +126,7 @@ function buildFormData(): FormData {
|
||||
appendToFormData(formData, (formData, index, variant) => {
|
||||
formData.append(`variants[${index}][name]`, variant.name.trim());
|
||||
formData.append(`variants[${index}][stock]`, String(Number.parseInt(variant.stock, 10) || 0));
|
||||
formData.append(`variants[${index}][stock_ecer]`, String(Number.parseInt(variant.stock_ecer, 10) || 0));
|
||||
formData.append(`variants[${index}][stock_retail]`, String(Number.parseInt(variant.stock_retail, 10) || 0));
|
||||
appendMediaToFormData(formData, `variants[${index}]`, variant.media);
|
||||
}, props.method);
|
||||
|
||||
@ -228,12 +228,12 @@ function submit() {
|
||||
<FieldError :errors="variantErrors(form, variant.client_id, 'stock')" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel :for="`variant_stock_ecer_${variant.client_id}`" required>
|
||||
<FieldLabel :for="`variant_stock_retail_${variant.client_id}`" required>
|
||||
Stok Ecer
|
||||
</FieldLabel>
|
||||
<NumberInput :id="`variant_stock_ecer_${variant.client_id}`" :model-value="variant.stock_ecer"
|
||||
@update:model-value="setVariantField(variant.client_id, 'stock_ecer', String($event))" />
|
||||
<FieldError :errors="variantErrors(form, variant.client_id, 'stock_ecer')" />
|
||||
<NumberInput :id="`variant_stock_retail_${variant.client_id}`" :model-value="variant.stock_retail"
|
||||
@update:model-value="setVariantField(variant.client_id, 'stock_retail', String($event))" />
|
||||
<FieldError :errors="variantErrors(form, variant.client_id, 'stock_retail')" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
|
||||
import { DataTableEmpty } from '@/components/data-table';
|
||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||
import StockEcerTransferModal from '@/components/modal/StockEcerTransferModal.vue';
|
||||
import StockRetailTransferModal from '@/components/modal/StockRetailTransferModal.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -72,20 +72,20 @@ function rowNumber(index: number): number {
|
||||
return (props.firstItem ?? 1) + index;
|
||||
}
|
||||
|
||||
// Stock Ecer Transfer Modal
|
||||
// Stock Retail Transfer Modal
|
||||
const transferModalOpen = ref(false);
|
||||
const transferVariantId = ref(0);
|
||||
const transferVariantName = ref('');
|
||||
const transferProductName = ref('');
|
||||
const transferStockBagus = ref(0);
|
||||
const transferStockEcer = ref(0);
|
||||
const transferStockRetail = ref(0);
|
||||
|
||||
function openTransferModal(product: ProductListItem, variant: Variant) {
|
||||
transferVariantId.value = variant.id;
|
||||
transferVariantName.value = variant.name;
|
||||
transferProductName.value = product.name;
|
||||
transferStockBagus.value = variant.stock;
|
||||
transferStockEcer.value = variant.stock_ecer;
|
||||
transferStockRetail.value = variant.stock_retail;
|
||||
transferModalOpen.value = true;
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ function onTransferSubmitted() {
|
||||
</span>
|
||||
<span>
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
@ -179,7 +179,7 @@ function onTransferSubmitted() {
|
||||
{{ formatStock(variant.reject_stock) }}
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums text-blue-600">
|
||||
{{ formatStock(variant.stock_ecer) }}
|
||||
{{ formatStock(variant.stock_retail) }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div v-if="variant.prices?.length" class="space-y-0.5 text-xs">
|
||||
@ -236,13 +236,13 @@ function onTransferSubmitted() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StockEcerTransferModal
|
||||
<StockRetailTransferModal
|
||||
v-model:open="transferModalOpen"
|
||||
:variant-id="transferVariantId"
|
||||
:variant-name="transferVariantName"
|
||||
:product-name="transferProductName"
|
||||
:stock-bagus="transferStockBagus"
|
||||
:stock-ecer="transferStockEcer"
|
||||
:stock-retail="transferStockRetail"
|
||||
@submitted="onTransferSubmitted"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -103,7 +103,7 @@ export const STOCK_QUALITY_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
export const STOCK_QUALITY_OPTIONS_CASHIER = [
|
||||
{ value: 'ecer', label: 'Eceran' },
|
||||
{ value: 'retail', label: 'Eceran' },
|
||||
] as const;
|
||||
|
||||
export type OrderEditItem = {
|
||||
|
||||
@ -3,7 +3,7 @@ export const PRICE_TYPES = [
|
||||
'agent',
|
||||
'sub_agent',
|
||||
'grosir',
|
||||
'ecer',
|
||||
'retail',
|
||||
'tiktok',
|
||||
'shopee',
|
||||
'harga_modal',
|
||||
@ -16,7 +16,7 @@ export const PRICE_TYPE_LABELS: Record<PriceType, string> = {
|
||||
agent: 'Agen',
|
||||
sub_agent: 'Sub Agen',
|
||||
grosir: 'Grosir',
|
||||
ecer: 'Eceran',
|
||||
retail: 'Eceran',
|
||||
tiktok: 'TikTok',
|
||||
shopee: 'Shopee',
|
||||
harga_modal: 'Harga Modal',
|
||||
@ -43,7 +43,7 @@ export interface Variant {
|
||||
name: string;
|
||||
stock: number;
|
||||
reject_stock: number;
|
||||
stock_ecer: number;
|
||||
stock_retail: number;
|
||||
prices: Price[];
|
||||
images: MediaItem[];
|
||||
}
|
||||
@ -95,6 +95,6 @@ export interface ProductVariantFormItem {
|
||||
id?: number;
|
||||
name: string;
|
||||
stock: string | number;
|
||||
stock_ecer: string | number;
|
||||
stock_retail: string | number;
|
||||
media: import('@/types/media').MediaUploadState;
|
||||
}
|
||||
|
||||
@ -14,16 +14,16 @@
|
||||
use App\Http\Controllers\Admin\Hr\AttendanceController;
|
||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
||||
use App\Http\Controllers\Admin\Manage\CuttingController;
|
||||
use App\Http\Controllers\Admin\Manage\CuttingDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\OrderController;
|
||||
use App\Http\Controllers\Admin\Manage\OrderDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\Cutting\CuttingController;
|
||||
use App\Http\Controllers\Admin\Manage\Cutting\CuttingDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\Order\OrderController;
|
||||
use App\Http\Controllers\Admin\Manage\Order\OrderDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\OwnerVerificationController;
|
||||
use App\Http\Controllers\Admin\Manage\PurchaseController;
|
||||
use App\Http\Controllers\Admin\Manage\PurchaseDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseController;
|
||||
use App\Http\Controllers\Admin\Manage\Purchase\PurchaseDraftItemController;
|
||||
use App\Http\Controllers\Admin\Manage\StokOpnameController;
|
||||
use App\Http\Controllers\Admin\Manage\StockController;
|
||||
use App\Http\Controllers\Admin\Manage\StockEcerController;
|
||||
use App\Http\Controllers\Admin\Manage\Stock\StockController;
|
||||
use App\Http\Controllers\Admin\Manage\Stock\StockRetailController;
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
@ -357,10 +357,10 @@
|
||||
->name('verify');
|
||||
});
|
||||
|
||||
Route::prefix('stock-ecer')->name('stock-ecer.')
|
||||
Route::prefix('stock-retail')->name('stock-retail.')
|
||||
->middleware('permission:'.Permission::STOCKS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::post('/transfer', [StockEcerController::class, 'transfer'])->name('transfer');
|
||||
Route::post('/transfer', [StockRetailController::class, 'transfer'])->name('transfer');
|
||||
});
|
||||
|
||||
Route::prefix('stok-opnames')->name('stok-opnames.')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user