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