From 9539ff70429a2c8553786c9508a2fdbe214a075e Mon Sep 17 00:00:00 2001
From: Yoga Pangestu
Date: Sun, 19 Jul 2026 17:29:19 +0700
Subject: [PATCH] refactor: remove StockController and related stock
verification logic
- Deleted `data-table-actions.vue` component as it is no longer needed.
- Removed `CuttingResultPriceItem` type and related properties from `cutting.ts`.
- Updated routes in `web.php` to eliminate stock verification routes and permissions.
- Removed `StockTest.php` file and its associated tests for stock verification and management.
- Adjusted `CuttingTest.php` to reflect changes in cutting results handling and removed references to product variants.
---
app/Enums/CuttingStatus.php | 38 +-
app/Enums/Permission.php | 7 +-
app/Enums/Role.php | 4 -
.../Manage/Cutting/CuttingController.php | 8 -
.../Cutting/CuttingDraftItemController.php | 6 +-
.../Manage/OwnerVerificationController.php | 29 --
.../Admin/Manage/Stock/StockController.php | 52 ---
app/Http/Middleware/HandleInertiaRequests.php | 21 -
.../Manage/CuttingDraftResultRequest.php | 7 +-
.../Requests/Admin/Manage/CuttingRequest.php | 20 +-
.../Manage/CuttingStatusTransitionRequest.php | 83 ----
.../Admin/Manage/StockVerifyRequest.php | 108 -----
app/Models/Cutting.php | 17 -
app/Models/CuttingResult.php | 5 -
app/Models/CuttingResultPrice.php | 63 ---
app/Models/ProductVariant.php | 5 -
.../Manage/CuttingResultPriceResolver.php | 82 +---
app/Services/Manage/CuttingService.php | 296 ++----------
.../Manage/OwnerVerificationService.php | 167 +------
app/Services/Manage/StockService.php | 422 +-----------------
app/Support/ActivityLog/ModelLabel.php | 2 -
database/factories/CuttingFactory.php | 7 -
database/factories/CuttingResultFactory.php | 3 +-
...duct_variant_nullable_add_product_name.php | 74 +++
...10000_drop_cutting_result_prices_table.php | 26 ++
database/seeders/CuttingResultSeeder.php | 5 +-
resources/js/components/AppSidebar.vue | 3 +-
resources/js/components/MobileBottomNav.vue | 2 +-
.../OwnerVerificationRowActions.vue | 21 +-
resources/js/constants/cutting-status.ts | 40 +-
.../js/pages/admin/manage/cuttings/Create.vue | 6 -
.../js/pages/admin/manage/cuttings/Edit.vue | 15 +-
.../js/pages/admin/manage/cuttings/Share.vue | 101 ++---
.../form/CuttingPosCartDetailDialog.vue | 7 +-
.../manage/cuttings/form/CuttingPosForm.vue | 127 ++----
.../form/CuttingPosResultCatalogPanel.vue | 176 --------
.../form/CuttingPosResultSummaryItems.vue | 88 +---
.../cuttings/form/CuttingPosSummaryPanel.vue | 16 +-
.../cuttings/form/QuickCreateProductModal.vue | 335 --------------
.../manage/cuttings/form/useCuttingPosCart.ts | 222 +--------
.../table/CuttingCompletedSection.vue | 9 +-
.../cuttings/table/CuttingGroupedTable.vue | 84 +---
.../table/CuttingInProgressSection.vue | 5 +-
.../cuttings/table/CuttingRejectDialog.vue | 92 ----
.../cuttings/table/CuttingVerifyDialog.vue | 241 ----------
.../cuttings/table/data-table-actions.vue | 26 +-
.../js/pages/admin/manage/stocks/Index.vue | 33 --
.../table/StockPendingApprovalSection.vue | 246 ----------
.../stocks/table/StockPendingSection.vue | 220 ---------
.../stocks/table/StockVerifiedSection.vue | 159 -------
.../manage/stocks/table/StockVerifyDialog.vue | 347 --------------
.../stocks/table/data-table-actions.vue | 22 -
resources/js/types/cutting.ts | 44 +-
routes/web.php | 23 +-
tests/Feature/Admin/Manage/CuttingTest.php | 157 ++-----
tests/Feature/Admin/Manage/StockTest.php | 364 ---------------
56 files changed, 357 insertions(+), 4431 deletions(-)
delete mode 100644 app/Http/Controllers/Admin/Manage/Stock/StockController.php
delete mode 100644 app/Http/Requests/Admin/Manage/StockVerifyRequest.php
delete mode 100644 app/Models/CuttingResultPrice.php
create mode 100644 database/migrations/2026_07_18_100150_make_cutting_results_product_variant_nullable_add_product_name.php
create mode 100644 database/migrations/2026_07_19_010000_drop_cutting_result_prices_table.php
delete mode 100644 resources/js/pages/admin/manage/cuttings/form/CuttingPosResultCatalogPanel.vue
delete mode 100644 resources/js/pages/admin/manage/cuttings/form/QuickCreateProductModal.vue
delete mode 100644 resources/js/pages/admin/manage/cuttings/table/CuttingRejectDialog.vue
delete mode 100644 resources/js/pages/admin/manage/cuttings/table/CuttingVerifyDialog.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/Index.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/table/StockPendingApprovalSection.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/table/StockPendingSection.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/table/StockVerifiedSection.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/table/StockVerifyDialog.vue
delete mode 100644 resources/js/pages/admin/manage/stocks/table/data-table-actions.vue
delete mode 100644 tests/Feature/Admin/Manage/StockTest.php
diff --git a/app/Enums/CuttingStatus.php b/app/Enums/CuttingStatus.php
index 81152da..1f0037c 100644
--- a/app/Enums/CuttingStatus.php
+++ b/app/Enums/CuttingStatus.php
@@ -11,33 +11,24 @@ enum CuttingStatus: string
case IN_PROGRESS = 'in_progress';
case COMPLETED = 'completed';
- case PENDING_VERIFICATION = 'pending_verification';
- case VERIFIED = 'verified';
- case REJECTED = 'rejected';
public function label(): string
{
return match ($this) {
self::IN_PROGRESS => 'Proses',
self::COMPLETED => 'Selesai',
- self::PENDING_VERIFICATION => 'Menunggu Verifikasi',
- self::VERIFIED => 'Terverifikasi',
- self::REJECTED => 'Ditolak',
};
}
public function isEditable(): bool
{
- return in_array($this, [self::IN_PROGRESS, self::REJECTED], true);
+ return $this === self::IN_PROGRESS;
}
public function transitionStatusMessage(): string
{
return match ($this) {
- self::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
- self::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
- self::REJECTED => 'Proses cutting berhasil ditolak.',
- self::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
+ self::COMPLETED => 'Proses cutting berhasil diselesaikan.',
default => 'Status proses cutting berhasil diperbarui.',
};
}
@@ -46,9 +37,6 @@ public function canTransitionTo(self $status): bool
{
return match ($this) {
self::IN_PROGRESS => $status === self::COMPLETED,
- self::COMPLETED => in_array($status, [self::PENDING_VERIFICATION, self::REJECTED, self::IN_PROGRESS, self::VERIFIED], true),
- self::PENDING_VERIFICATION => in_array($status, [self::VERIFIED, self::REJECTED, self::COMPLETED], true),
- self::REJECTED => $status === self::IN_PROGRESS,
default => false,
};
}
@@ -57,9 +45,6 @@ public function transitionPermission(): Permission
{
return match ($this) {
self::COMPLETED => Permission::CUTTINGS_COMPLETE,
- self::PENDING_VERIFICATION => Permission::CUTTINGS_VERIFY,
- self::VERIFIED => Permission::CUTTINGS_VERIFY,
- self::REJECTED => Permission::CUTTINGS_REJECT,
self::IN_PROGRESS => Permission::CUTTINGS_UPDATE,
default => throw new InvalidArgumentException('Status tidak mendukung transisi.'),
};
@@ -80,25 +65,6 @@ public function availableActions(): array
'icon_only' => false,
],
],
- self::COMPLETED => [
- [
- 'status' => self::VERIFIED->value,
- 'label' => 'Verifikasi',
- 'destructive' => false,
- 'permission' => Permission::CUTTINGS_VERIFY->value,
- 'icon_only' => false,
- ],
- ],
- self::PENDING_VERIFICATION => [],
- self::REJECTED => [
- [
- 'status' => self::IN_PROGRESS->value,
- 'label' => 'Kembalikan ke Proses',
- 'destructive' => false,
- 'permission' => Permission::CUTTINGS_UPDATE->value,
- 'icon_only' => false,
- ],
- ],
default => [],
};
}
diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php
index 024f11b..2e88661 100644
--- a/app/Enums/Permission.php
+++ b/app/Enums/Permission.php
@@ -103,8 +103,6 @@ enum Permission: string
case CUTTINGS_UPDATE = 'cuttings.update';
case CUTTINGS_DELETE = 'cuttings.delete';
case CUTTINGS_COMPLETE = 'cuttings.complete';
- case CUTTINGS_VERIFY = 'cuttings.verify';
- case CUTTINGS_REJECT = 'cuttings.reject';
case OWNER_VERIFICATIONS_VIEW = 'owner_verifications.view';
case OWNER_VERIFICATIONS_VERIFY = 'owner_verifications.verify';
@@ -256,8 +254,6 @@ public function label(): string
self::CUTTINGS_UPDATE => 'Ubah Cutting',
self::CUTTINGS_DELETE => 'Hapus Cutting',
self::CUTTINGS_COMPLETE => 'Selesaikan Cutting',
- self::CUTTINGS_VERIFY => 'Verifikasi Cutting',
- self::CUTTINGS_REJECT => 'Tolak Cutting',
self::OWNER_VERIFICATIONS_VIEW => 'Lihat Verifikasi Owner',
self::OWNER_VERIFICATIONS_VERIFY => 'Setujui Verifikasi Owner',
@@ -349,8 +345,7 @@ public function group(): string
self::ORDERS_DELETE, self::ORDERS_SEND, self::ORDERS_COMPLETE,
self::ORDERS_CANCEL => 'Pesanan',
self::CUTTINGS_VIEW, self::CUTTINGS_CREATE, self::CUTTINGS_UPDATE,
- self::CUTTINGS_DELETE, self::CUTTINGS_COMPLETE, self::CUTTINGS_VERIFY,
- self::CUTTINGS_REJECT => 'Cutting',
+ self::CUTTINGS_DELETE, self::CUTTINGS_COMPLETE,
self::OWNER_VERIFICATIONS_VIEW, self::OWNER_VERIFICATIONS_VERIFY,
self::OWNER_VERIFICATIONS_REJECT => 'Verifikasi Owner',
self::STOCKS_VIEW,
diff --git a/app/Enums/Role.php b/app/Enums/Role.php
index eba8dce..f59a5a1 100644
--- a/app/Enums/Role.php
+++ b/app/Enums/Role.php
@@ -141,8 +141,6 @@ public function permissions(): array
Permission::STOK_OPNAMES_VIEW,
- Permission::CUTTINGS_VERIFY,
-
Permission::ATTENDANCES_VIEW,
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
@@ -307,8 +305,6 @@ public function permissions(): array
Permission::CUTTINGS_UPDATE,
Permission::CUTTINGS_DELETE,
Permission::CUTTINGS_COMPLETE,
- Permission::CUTTINGS_VERIFY,
-
Permission::PURCHASES_VIEW,
Permission::PURCHASES_CREATE,
Permission::PURCHASES_UPDATE,
diff --git a/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php b/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
index 287db94..dd8536a 100644
--- a/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
+++ b/app/Http/Controllers/Admin/Manage/Cutting/CuttingController.php
@@ -9,7 +9,6 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\CuttingRequest;
use App\Http\Requests\Admin\Manage\CuttingStatusTransitionRequest;
-use App\Models\Category;
use App\Models\Cutting;
use App\Services\Manage\CuttingService;
use Illuminate\Http\RedirectResponse;
@@ -43,10 +42,8 @@ public function create(Request $request): Response
return Inertia::render('admin/manage/cuttings/Create', [
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user),
- 'productCatalog' => $this->cuttingService->productCatalog(user: $user),
'draftMaterials' => $this->cuttingService->draftMaterialsForUser($user),
'draftResults' => $this->cuttingService->draftResultsForUser($user),
- 'categories' => Category::query()->orderBy('name')->get(['id', 'name'])->map(fn ($c) => ['value' => $c->id, 'label' => $c->name])->all(),
'units' => collect(RawMaterialUnit::cases())->map(fn ($u) => ['value' => $u->value, 'label' => $u->label()])->all(),
]);
}
@@ -73,8 +70,6 @@ public function edit(Cutting $cutting): Response|RedirectResponse
return Inertia::render('admin/manage/cuttings/Edit', [
'cutting' => $this->cuttingService->findForEdit($cutting),
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog($cutting),
- 'productCatalog' => $this->cuttingService->productCatalog($cutting),
- 'categories' => Category::query()->orderBy('name')->get(['id', 'name'])->map(fn ($c) => ['value' => $c->id, 'label' => $c->name])->all(),
'units' => collect(RawMaterialUnit::cases())->map(fn ($u) => ['value' => $u->value, 'label' => $u->label()])->all(),
]);
}
@@ -106,9 +101,6 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
$status,
$request->user(),
$request->validated('reason'),
- $request->validated('verification_note'),
- $request->validated('results'),
- $request->validated('result_prices'),
);
$this->flashSuccess($status->transitionStatusMessage());
diff --git a/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php b/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
index 7b48024..cce2dd0 100644
--- a/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
+++ b/app/Http/Controllers/Admin/Manage/Cutting/CuttingDraftItemController.php
@@ -7,7 +7,7 @@
use App\Http\Requests\Admin\Manage\CuttingDraftMaterialRequest;
use App\Http\Requests\Admin\Manage\CuttingDraftResultRequest;
use App\Http\Requests\Admin\Manage\CuttingQuickCreateProductRequest;
-use App\Models\ProductVariant;
+use App\Models\CuttingResult;
use App\Models\RawMaterialPrice;
use App\Services\Manage\CuttingService;
use Illuminate\Http\JsonResponse;
@@ -40,9 +40,9 @@ public function destroyMaterial(Request $request, RawMaterialPrice $rawMaterialP
return response()->json(['ok' => true]);
}
- public function destroyResult(Request $request, ProductVariant $productVariant): JsonResponse
+ public function destroyResult(Request $request, CuttingResult $cuttingResult): JsonResponse
{
- $this->cuttingService->removeDraftResult($request->user(), $productVariant);
+ $this->cuttingService->removeDraftResult($request->user(), $cuttingResult);
return response()->json(['ok' => true]);
}
diff --git a/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php b/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php
index 78cca97..a6ff2f3 100644
--- a/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php
+++ b/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php
@@ -6,10 +6,8 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\ApproveVerificationRequest;
use App\Http\Requests\Admin\Manage\RejectVerificationRequest;
-use App\Models\Cutting;
use App\Models\OwnerVerificationRequest;
use App\Services\Manage\OwnerVerificationService;
-use App\Services\Manage\StockService;
use App\Support\ActivityLog\ModelLabel;
use App\Support\OwnerVerification\VerificationChangeFormatter;
use Illuminate\Http\JsonResponse;
@@ -21,7 +19,6 @@ class OwnerVerificationController extends Controller
public function __construct(
private readonly OwnerVerificationService $ownerVerificationService,
- private readonly StockService $stockService,
) {}
public function show(OwnerVerificationRequest $ownerVerificationRequest): JsonResponse
@@ -40,32 +37,6 @@ public function show(OwnerVerificationRequest $ownerVerificationRequest): JsonRe
]);
}
- public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
- {
- $this->stockService->approveVerification(
- $cutting,
- $request->user(),
- $request->validated('approval_note'),
- );
-
- $this->flashSuccess('Verifikasi cutting berhasil disetujui. Stok produk telah ditambahkan ke toko.');
-
- return back(302);
- }
-
- public function rejectCutting(RejectVerificationRequest $request, Cutting $cutting): RedirectResponse
- {
- $this->stockService->rejectVerification(
- $cutting,
- $request->user(),
- $request->validated('reason'),
- );
-
- $this->flashSuccess('Verifikasi cutting berhasil ditolak.');
-
- return back(302);
- }
-
public function approveRequest(
ApproveVerificationRequest $request,
OwnerVerificationRequest $ownerVerificationRequest,
diff --git a/app/Http/Controllers/Admin/Manage/Stock/StockController.php b/app/Http/Controllers/Admin/Manage/Stock/StockController.php
deleted file mode 100644
index 62daf7d..0000000
--- a/app/Http/Controllers/Admin/Manage/Stock/StockController.php
+++ /dev/null
@@ -1,52 +0,0 @@
-user();
-
- return Inertia::render('admin/manage/stocks/Index', [
- 'pendingCuttings' => $this->stockService->getPendingVerificationCuttings($user),
- 'pendingApprovalCuttings' => $this->stockService->getPendingApprovalCuttings($user),
- ]);
- }
-
- public function verify(StockVerifyRequest $request, Cutting $cutting): RedirectResponse
- {
- $this->stockService->submitVerification(
- $cutting,
- $request->user(),
- $request->validated('verification_note'),
- $request->validated('results'),
- $request->validated('result_prices'),
- );
-
- if ($request->user()->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
- $this->flashSuccess('Verifikasi berhasil disimpan. Stok produk telah ditambahkan ke toko.');
- } else {
- $this->flashSuccess('Verifikasi berhasil diajukan. Menunggu persetujuan owner.');
- }
-
- return redirect()->route('admin.manage.stocks.index');
- }
-}
diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php
index 3f58b18..b6202f5 100644
--- a/app/Http/Middleware/HandleInertiaRequests.php
+++ b/app/Http/Middleware/HandleInertiaRequests.php
@@ -2,7 +2,6 @@
namespace App\Http\Middleware;
-use App\Models\Cutting;
use App\Models\EmployeeAdvance;
use App\Models\LeaveRequest;
use App\Services\System\Setting\SystemService;
@@ -70,7 +69,6 @@ public function share(Request $request): array
'vapidPublicKey' => config('webpush.vapid.public_key'),
'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request),
'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request),
- 'pendingCuttings' => fn () => $this->pendingCuttings($request),
];
}
@@ -99,23 +97,4 @@ private function pendingEmployeeAdvances(Request $request): int
->pending()
->count();
}
-
- private function pendingCuttings(Request $request): int
- {
- $user = $request->user();
-
- if ($user === null || ! $user->can('stocks.view')) {
- return 0;
- }
-
- if ($user->can('owner_verifications.verify')) {
- return Cutting::query()
- ->pendingVerification()
- ->count();
- }
-
- return Cutting::query()
- ->completed()
- ->count();
- }
}
diff --git a/app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php b/app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php
index f95614b..fe88d81 100644
--- a/app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php
+++ b/app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php
@@ -4,7 +4,6 @@
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
-use Illuminate\Validation\Rule;
class CuttingDraftResultRequest extends FormRequest
{
@@ -19,11 +18,7 @@ public function authorize(): bool
public function rules(): array
{
return [
- 'product_variant_id' => [
- 'required',
- 'integer',
- Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
- ],
+ 'product_name' => ['required', 'string', 'max:255'],
'cutting_result' => ['nullable', 'integer', 'min:1'],
'sample' => ['nullable', 'integer', 'min:0'],
'original_outside_sample' => ['nullable', 'integer', 'min:0'],
diff --git a/app/Http/Requests/Admin/Manage/CuttingRequest.php b/app/Http/Requests/Admin/Manage/CuttingRequest.php
index e58a5a2..e92dd58 100644
--- a/app/Http/Requests/Admin/Manage/CuttingRequest.php
+++ b/app/Http/Requests/Admin/Manage/CuttingRequest.php
@@ -45,19 +45,15 @@ public function rules(): array
$rules['materials.*.material_result'] = ['nullable', 'integer', 'min:0'];
$rules['materials.*.combination_id'] = ['nullable', 'integer'];
$rules['materials.*.combination_material_result'] = ['nullable', 'integer', 'min:0'];
-
- $rules['results'] = ['required', 'array', 'min:1'];
- $rules['results.*.product_variant_id'] = [
- 'required',
- 'integer',
- 'distinct',
- Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
- ];
- $rules['results.*.cutting_result'] = ['required', 'integer', 'min:1'];
- $rules['results.*.sample'] = ['required', 'integer', 'min:0'];
- $rules['results.*.original_outside_sample'] = ['required', 'integer', 'min:0'];
}
+ $rules['results'] = ['required', 'array', 'min:1'];
+ $rules['results.*.product_name'] = ['required', 'string', 'max:255'];
+
+ $rules['results.*.cutting_result'] = ['required', 'integer', 'min:1'];
+ $rules['results.*.sample'] = ['required', 'integer', 'min:0'];
+ $rules['results.*.original_outside_sample'] = ['required', 'integer', 'min:0'];
+
return array_merge(
$rules,
$this->photoRules('photos', 10),
@@ -78,7 +74,7 @@ public function attributes(): array
'materials.*.material_result' => 'hasil',
'materials.*.combination_material_result' => 'hasil kombinasi',
'results' => 'hasil produk',
- 'results.*.product_variant_id' => 'varian produk',
+ 'results.*.product_name' => 'nama produk',
'results.*.cutting_result' => 'hasil',
'results.*.sample' => 'sample',
'results.*.original_outside_sample' => 'diluar sample',
diff --git a/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php b/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php
index b4af0f7..97ceef4 100644
--- a/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php
+++ b/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php
@@ -3,9 +3,6 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\CuttingStatus;
-use App\Enums\Permission;
-use App\Enums\PriceType;
-use App\Enums\Role;
use App\Models\Cutting;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -21,12 +18,6 @@ public function authorize(): bool
return false;
}
- if ($status === CuttingStatus::IN_PROGRESS) {
- return $this->user()?->can(Permission::CUTTINGS_REJECT->value)
- || $this->user()?->can(Permission::CUTTINGS_UPDATE->value)
- || false;
- }
-
try {
$permission = $status->transitionPermission();
} catch (\InvalidArgumentException) {
@@ -43,29 +34,6 @@ public function rules(): array
{
return [
'status' => ['required', Rule::enum(CuttingStatus::class)],
- 'reason' => ['nullable', 'string', 'max:500'],
- 'verification_note' => ['nullable', 'string', 'max:500'],
- 'results' => ['nullable', 'array'],
- 'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'],
- 'results.*.sample' => ['required_with:results', 'integer', 'min:0'],
- 'results.*.original_outside_sample' => ['required_with:results', 'integer', 'min:0'],
- 'result_prices' => ['nullable', 'array'],
- 'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
- 'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
- 'result_prices.*.prices.*.type' => ['required', Rule::enum(PriceType::class)],
- 'result_prices.*.prices.*.price' => ['required', 'integer', 'gt:0'],
- ];
- }
-
- /**
- * @return array
- */
- public function attributes(): array
- {
- return [
- 'status' => 'status',
- 'reason' => 'alasan penolakan',
- 'verification_note' => 'catatan verifikasi',
];
}
@@ -83,57 +51,6 @@ public function withValidator(Validator $validator): void
if (! $cutting->status->canTransitionTo($status)) {
$validator->errors()->add('status', 'Status cutting tidak dapat diubah.');
}
-
- // Only owner/developer can verify directly from COMPLETED status
- if ($cutting->status === CuttingStatus::COMPLETED && $status === CuttingStatus::VERIFIED) {
- $user = $this->user();
- if (! $user || ! $user->hasRole([Role::OWNER->value, Role::DEVELOPER->value])) {
- $validator->errors()->add('status', 'Hanya owner yang dapat langsung memverifikasi cutting.');
- }
- }
-
- if ($status === CuttingStatus::REJECTED && trim((string) $this->input('reason')) === '') {
- $validator->errors()->add('reason', 'Alasan penolakan wajib diisi.');
- }
-
- if ($status === CuttingStatus::VERIFIED) {
- if ($this->has('results')) {
- $cuttingResults = $cutting->results->keyBy('product_variant_id');
- foreach ($this->input('results', []) as $index => $item) {
- $variantId = $item['product_variant_id'] ?? 0;
- $sample = (int) ($item['sample'] ?? 0);
- $originalOutsideSample = (int) ($item['original_outside_sample'] ?? 0);
-
- $originalResult = $cuttingResults->get($variantId);
- if ($originalResult === null) {
- $validator->errors()->add("results.{$index}.product_variant_id", 'Varian produk tidak ditemukan pada cutting ini.');
-
- continue;
- }
-
- if (($sample + $originalOutsideSample) !== (int) $originalResult->cutting_result) {
- $validator->errors()->add("results.{$index}.sample", "Total jumlah (sample + hasil cutting diluar sample) harus sama dengan hasil cutting asli ({$originalResult->cutting_result} pcs).");
- }
- }
- }
-
- if (! $this->has('result_prices') || $this->input('result_prices') === []) {
- $validator->errors()->add('result_prices', 'Harga jual wajib diisi saat verifikasi.');
- } else {
- $variantIds = $cutting->results->pluck('product_variant_id')->all();
- $submittedVariantIds = collect($this->input('result_prices', []))
- ->pluck('product_variant_id')
- ->map(fn ($id) => (int) $id)
- ->all();
-
- foreach ($variantIds as $variantId) {
- if (! in_array($variantId, $submittedVariantIds, true)) {
- $validator->errors()->add('result_prices', 'Harga jual wajib diisi untuk semua varian hasil cutting.');
- break;
- }
- }
- }
- }
});
}
}
diff --git a/app/Http/Requests/Admin/Manage/StockVerifyRequest.php b/app/Http/Requests/Admin/Manage/StockVerifyRequest.php
deleted file mode 100644
index 313822b..0000000
--- a/app/Http/Requests/Admin/Manage/StockVerifyRequest.php
+++ /dev/null
@@ -1,108 +0,0 @@
-user()?->can(Permission::CUTTINGS_VERIFY->value) ?? false;
- }
-
- /**
- * @return array
- */
- public function rules(): array
- {
- return [
- 'verification_note' => ['nullable', 'string', 'max:500'],
- 'results' => ['nullable', 'array'],
- 'results.*.product_variant_id' => ['required_with:results', 'integer', 'exists:product_variants,id'],
- 'results.*.good' => ['required_with:results', 'integer', 'min:0'],
- 'results.*.reject' => ['required_with:results', 'integer', 'min:0'],
- 'result_prices' => ['nullable', 'array'],
- 'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
- 'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
- 'result_prices.*.prices.*.type' => ['required', Rule::enum(PriceType::class)],
- 'result_prices.*.prices.*.price' => ['required', 'integer', 'min:0'],
- ];
- }
-
- /**
- * @return array
- */
- public function attributes(): array
- {
- return [
- 'verification_note' => 'catatan verifikasi',
- 'result_prices' => 'harga jual',
- 'result_prices.*.product_variant_id' => 'varian produk',
- 'result_prices.*.prices' => 'harga jual',
- 'result_prices.*.prices.*.type' => 'tipe harga',
- 'result_prices.*.prices.*.price' => 'harga jual',
- 'results' => 'hasil cutting',
- 'results.*.product_variant_id' => 'varian produk',
- 'results.*.good' => 'bagus',
- 'results.*.reject' => 'reject',
- ];
- }
-
- public function withValidator(Validator $validator): void
- {
- $validator->after(function (Validator $validator): void {
- /** @var Cutting $cutting */
- $cutting = $this->route('cutting');
-
- if ($cutting->status !== CuttingStatus::COMPLETED) {
- $validator->errors()->add('status', 'Hanya cutting yang sudah selesai yang dapat diverifikasi.');
-
- return;
- }
-
- if ($this->has('results')) {
- $cuttingResults = $cutting->results->keyBy('product_variant_id');
- foreach ($this->input('results', []) as $index => $item) {
- $variantId = $item['product_variant_id'] ?? 0;
- $good = (int) ($item['good'] ?? 0);
- $reject = (int) ($item['reject'] ?? 0);
-
- $originalResult = $cuttingResults->get($variantId);
- if ($originalResult === null) {
- $validator->errors()->add("results.{$index}.product_variant_id", 'Varian produk tidak ditemukan pada cutting ini.');
-
- continue;
- }
-
- if (($good + $reject) > (int) $originalResult->cutting_result) {
- $validator->errors()->add("results.{$index}.good", "Total jumlah (bagus + reject) tidak boleh melebihi hasil cutting asli ({$originalResult->cutting_result} pcs).");
- }
- }
- }
-
- if (! $this->has('result_prices') || $this->input('result_prices') === []) {
- $validator->errors()->add('result_prices', 'Harga jual wajib diisi saat verifikasi.');
- } else {
- $variantIds = $cutting->results->pluck('product_variant_id')->all();
- $submittedVariantIds = collect($this->input('result_prices', []))
- ->pluck('product_variant_id')
- ->map(fn ($id) => (int) $id)
- ->all();
-
- foreach ($variantIds as $variantId) {
- if (! in_array($variantId, $submittedVariantIds, true)) {
- $validator->errors()->add('result_prices', 'Harga jual wajib diisi untuk semua varian hasil cutting.');
- break;
- }
- }
- }
- });
- }
-}
diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php
index 2b4cfc9..e12d886 100644
--- a/app/Models/Cutting.php
+++ b/app/Models/Cutting.php
@@ -55,18 +55,6 @@ protected function inProgress(Builder $query): void
$query->where('status', CuttingStatus::IN_PROGRESS);
}
- #[Scope]
- protected function pendingVerification(Builder $query): void
- {
- $query->where('status', CuttingStatus::PENDING_VERIFICATION);
- }
-
- #[Scope]
- protected function verified(Builder $query): void
- {
- $query->where('status', CuttingStatus::VERIFIED);
- }
-
// 4. Attribute
public function createdAtFormatted(): Attribute
{
@@ -118,11 +106,6 @@ public function combinations(): HasMany
return $this->hasMany(CuttingMaterialCombination::class);
}
- public function resultPrices(): HasMany
- {
- return $this->hasMany(CuttingResultPrice::class);
- }
-
public function results(): HasMany
{
return $this->hasMany(CuttingResult::class);
diff --git a/app/Models/CuttingResult.php b/app/Models/CuttingResult.php
index 9d8778e..77648fb 100644
--- a/app/Models/CuttingResult.php
+++ b/app/Models/CuttingResult.php
@@ -31,11 +31,6 @@ public function cutting(): BelongsTo
return $this->belongsTo(Cutting::class)->withTrashed();
}
- public function productVariant(): BelongsTo
- {
- return $this->belongsTo(ProductVariant::class)->withTrashed();
- }
-
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withTrashed();
diff --git a/app/Models/CuttingResultPrice.php b/app/Models/CuttingResultPrice.php
deleted file mode 100644
index bb51a1f..0000000
--- a/app/Models/CuttingResultPrice.php
+++ /dev/null
@@ -1,63 +0,0 @@
- 'integer',
- 'price' => 'integer',
- 'price_type' => PriceType::class,
- ];
- }
-
- // 3. Attribute
- public function costPerUnitFormatted(): Attribute
- {
- return Attribute::make(
- get: fn () => 'Rp '.number_format($this->cost_per_unit, 0, ',', '.'),
- );
- }
-
- public function priceFormatted(): Attribute
- {
- return Attribute::make(
- get: fn () => 'Rp '.number_format($this->price, 0, ',', '.'),
- );
- }
-
- public function typeLabel(): Attribute
- {
- return Attribute::make(
- get: fn () => $this->price_type->label(),
- );
- }
-
- // 4. Relation
- public function cutting(): BelongsTo
- {
- return $this->belongsTo(Cutting::class)->withTrashed();
- }
-
- public function productVariant(): BelongsTo
- {
- return $this->belongsTo(ProductVariant::class)->withTrashed();
- }
-}
diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php
index b539200..1dfe96f 100644
--- a/app/Models/ProductVariant.php
+++ b/app/Models/ProductVariant.php
@@ -72,11 +72,6 @@ public function registerMediaCollections(): void
}
// 6. Relation
- public function cuttingResultPrices(): HasMany
- {
- return $this->hasMany(CuttingResultPrice::class);
- }
-
public function cuttingResults(): HasMany
{
return $this->hasMany(CuttingResult::class);
diff --git a/app/Services/Manage/CuttingResultPriceResolver.php b/app/Services/Manage/CuttingResultPriceResolver.php
index a3b3d93..a9ceb58 100644
--- a/app/Services/Manage/CuttingResultPriceResolver.php
+++ b/app/Services/Manage/CuttingResultPriceResolver.php
@@ -3,7 +3,6 @@
namespace App\Services\Manage;
use App\Enums\PriceType;
-use App\Models\CuttingResultPrice;
use App\Models\ProductPrice;
use App\Services\Concerns\CachesQuery;
use Illuminate\Support\Collection;
@@ -12,56 +11,27 @@ class CuttingResultPriceResolver
{
use CachesQuery;
- public function resolve(int $productVariantId, PriceType $priceType): ?CuttingResultPrice
+ public function resolve(int $productVariantId, PriceType $priceType): ?ProductPrice
{
- $price = CuttingResultPrice::query()
- ->where('product_variant_id', $productVariantId)
- ->where('price_type', $priceType)
- ->whereHas('cutting', fn ($query) => $query->verified())
- ->join('cuttings', 'cutting_result_prices.cutting_id', '=', 'cuttings.id')
- ->orderByDesc('cuttings.created_at')
- ->select('cutting_result_prices.*')
- ->first();
-
- if ($price !== null) {
- return $price;
- }
-
- $productPrice = ProductPrice::query()
+ return ProductPrice::query()
->where('variant_id', $productVariantId)
->where('type', $priceType)
->first();
-
- if ($productPrice !== null) {
- $cp = new CuttingResultPrice;
- $cp->product_variant_id = $productVariantId;
- $cp->price_type = $priceType;
- $cp->price = $productPrice->price;
-
- return $cp;
- }
-
- return null;
}
- public function latestPricesForVariant(int $productVariantId): array
+ public function latestPricesForVariant(int $productVariantId): Collection
{
return $this->cacheRemember("prices:variant:{$productVariantId}", 900, function () use ($productVariantId) {
- $prices = [];
-
- foreach (PriceType::cases() as $priceType) {
- $price = $this->resolve($productVariantId, $priceType);
-
- if ($price !== null) {
- $prices[] = [
- 'price_type' => $price->price_type->value,
- 'price' => (int) $price->price,
- 'price_formatted' => $price->price_formatted,
- ];
- }
- }
-
- return $prices;
+ return ProductPrice::query()
+ ->where('variant_id', $productVariantId)
+ ->get()
+ ->map(fn (ProductPrice $price) => (object) [
+ 'price_type' => $price->type,
+ 'price' => $price->price,
+ 'price_formatted' => $price->price_formatted,
+ 'cost_per_unit' => 0,
+ 'cost_per_unit_formatted' => 'Rp 0',
+ ]);
});
}
@@ -72,40 +42,21 @@ public function latestPricesForVariants(array $variantIds): Collection
return [];
}
- $cuttingPrices = CuttingResultPrice::query()
- ->whereIn('product_variant_id', $variantIds)
- ->whereHas('cutting', fn ($query) => $query->verified())
- ->join('cuttings', 'cutting_result_prices.cutting_id', '=', 'cuttings.id')
- ->orderByDesc('cuttings.created_at')
- ->select('cutting_result_prices.*')
- ->get()
- ->groupBy(fn (CuttingResultPrice $price) => $price->product_variant_id.'-'.$price->price_type->value)
- ->map(fn (Collection $group) => $group->first());
-
$productPrices = ProductPrice::query()
->whereIn('variant_id', $variantIds)
->get()
->groupBy(fn (ProductPrice $price) => $price->variant_id.'-'.$price->type->value);
- // Store only plain arrays — never Eloquent models — to avoid
- // __PHP_Incomplete_Class when Redis deserializes across requests.
$results = [];
foreach ($variantIds as $variantId) {
foreach (PriceType::cases() as $priceType) {
$key = $variantId.'-'.$priceType->value;
- if ($cuttingPrices->has($key)) {
- $cp = $cuttingPrices->get($key);
- $results[$variantId][] = [
- 'price_type' => $cp->price_type->value,
- 'price' => (int) $cp->price,
- 'price_formatted' => $cp->price_formatted,
- ];
- } elseif ($productPrices->has($key)) {
+ if ($productPrices->has($key)) {
$pp = $productPrices->get($key)->first();
$results[$variantId][] = [
- 'price_type' => $priceType->value,
+ 'price_type' => $pp->type->value,
'price' => (int) $pp->price,
- 'price_formatted' => 'Rp '.number_format((int) $pp->price, 0, ',', '.'),
+ 'price_formatted' => $pp->price_formatted,
];
}
}
@@ -114,7 +65,6 @@ public function latestPricesForVariants(array $variantIds): Collection
return $results;
});
- // Rebuild as a Collection keyed by variant ID (matching original contract)
return collect(is_array($cached) ? $cached : [])->map(fn ($prices) => collect($prices));
}
}
diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php
index e2aed66..5c3a1a2 100644
--- a/app/Services/Manage/CuttingService.php
+++ b/app/Services/Manage/CuttingService.php
@@ -4,14 +4,11 @@
use App\Enums\CuttingStatus;
use App\Enums\RawMaterialUnit;
-use App\Enums\Role;
use App\Models\Cutting;
use App\Models\CuttingMaterial;
use App\Models\CuttingMaterialCombination;
use App\Models\CuttingResult;
-use App\Models\CuttingResultPrice;
use App\Models\Product;
-use App\Models\ProductVariant;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\User;
@@ -44,8 +41,6 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
])
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search'];
@@ -55,9 +50,8 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$query->where('variant', 'like', "%{$search}%")
->orWhereHas('rawMaterial', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
})
- ->orWhereHas('results.productVariant', function (Builder $query) use ($search): void {
- $query->where('name', 'like', "%{$search}%")
- ->orWhereHas('product', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
+ ->orWhereHas('results', function (Builder $query) use ($search): void {
+ $query->where('product_name', 'like', "%{$search}%");
});
});
});
@@ -88,8 +82,6 @@ public function getInProgressCuttings(User $user): Collection
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
])
->inProgress()
->latest()
@@ -113,8 +105,6 @@ public function getCompletedCuttings(User $user): Collection
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
])
->completed()
->latest()
@@ -162,40 +152,6 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
});
}
- public function productCatalog(?Cutting $cutting = null, ?User $user = null): Collection
- {
- $selectedVariantIds = $cutting
- ? $cutting->results()->pluck('product_variant_id')->all()
- : ($user ? $this->draftResultsQuery($user)->pluck('product_variant_id')->all() : []);
-
- return Product::query()
- ->with([
- 'variants' => fn ($query) => $query
- ->with('media')
- ->orderBy('created_at'),
- ])
- ->where(function (Builder $query) use ($selectedVariantIds): void {
- $query->active();
-
- if ($selectedVariantIds !== []) {
- $query->orWhereHas(
- 'variants',
- fn (Builder $query) => $query->whereIn('id', $selectedVariantIds),
- );
- }
- })
- ->orderBy('name')
- ->get()
- ->each(function (Product $product): void {
- $product->variants->each(function (ProductVariant $variant): void {
- $variant->setAttribute(
- 'images',
- MediaPresenter::collection($variant, 'images'),
- );
- });
- });
- }
-
public function findForEdit(Cutting $cutting): Cutting
{
$cutting->load([
@@ -203,8 +159,6 @@ public function findForEdit(Cutting $cutting): Cutting
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
'rejection.rejectedBy.profile',
]);
@@ -222,8 +176,6 @@ public function findForShare(Cutting $cutting): Cutting
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
]);
$this->appendCostPreview($cutting);
@@ -246,14 +198,7 @@ private function appendImages(Cutting $cutting): void
});
$cutting->results->each(function (CuttingResult $result): void {
- $variant = $result->productVariant;
-
- if ($variant) {
- $variant->setAttribute(
- 'images',
- MediaPresenter::collection($variant, 'images'),
- );
- }
+ $result->setAttribute('product_name', $result->product_name ?? '');
});
}
@@ -279,10 +224,6 @@ public function draftMaterialsForUser(User $user): array
public function draftResultsForUser(User $user): array
{
return $this->draftResultsQuery($user)
- ->with([
- 'productVariant.product:id,name',
- 'productVariant.media',
- ])
->get()
->map(fn (CuttingResult $item) => $this->presentDraftResult($item))
->values()
@@ -332,9 +273,7 @@ public function syncDraftMaterial(array $validated, User $user): array
public function syncDraftResult(array $validated, User $user): array
{
- $variant = ProductVariant::query()
- ->with('product:id,name')
- ->findOrFail($validated['product_variant_id']);
+ $productName = $validated['product_name'] ?? null;
$cuttingResult = array_key_exists('cutting_result', $validated) && $validated['cutting_result'] !== null
? (int) $validated['cutting_result']
@@ -372,22 +311,13 @@ public function syncDraftResult(array $validated, User $user): array
}
}
- $item = CuttingResult::query()->updateOrCreate(
- [
- 'user_id' => $user->id,
- 'product_variant_id' => $variant->id,
- 'cutting_id' => null,
- ],
- [
- 'cutting_result' => $cuttingResult,
- 'sample' => $sample,
- 'original_outside_sample' => $originalOutsideSample,
- ],
- );
-
- $item->load([
- 'productVariant.product:id,name',
- 'productVariant.media',
+ $item = CuttingResult::query()->create([
+ 'user_id' => $user->id,
+ 'cutting_id' => null,
+ 'product_name' => $productName,
+ 'cutting_result' => $cuttingResult,
+ 'sample' => $sample,
+ 'original_outside_sample' => $originalOutsideSample,
]);
return $this->presentDraftResult($item);
@@ -402,13 +332,13 @@ public function removeDraftMaterial(User $user, RawMaterialPrice $rawMaterialPri
->delete();
}
- public function removeDraftResult(User $user, ProductVariant $productVariant): void
+ public function removeDraftResult(User $user, CuttingResult $cuttingResult): void
{
- CuttingResult::query()
- ->whereNull('cutting_id')
- ->where('user_id', $user->id)
- ->where('product_variant_id', $productVariant->id)
- ->delete();
+ if ($cuttingResult->cutting_id !== null || $cuttingResult->user_id !== $user->id) {
+ return;
+ }
+
+ $cuttingResult->delete();
}
/**
@@ -491,22 +421,12 @@ function () use ($validated, $user): Cutting {
->lockForUpdate()
->get();
- $draftResults = $this->draftResultsQuery($user)
- ->lockForUpdate()
- ->get();
-
if ($draftMaterials->isEmpty()) {
throw ValidationException::withMessages([
'materials' => 'Tambahkan minimal satu bahan baku.',
]);
}
- if ($draftResults->isEmpty()) {
- throw ValidationException::withMessages([
- 'results' => 'Tambahkan minimal satu hasil produk.',
- ]);
- }
-
$cutting = Cutting::create([
'status' => CuttingStatus::IN_PROGRESS,
'description' => $validated['description'] ?? null,
@@ -536,10 +456,9 @@ function () use ($validated, $user): Cutting {
$material->save();
}
- foreach ($draftResults as $result) {
- $result->cutting_id = $cutting->id;
- $result->user_id = null;
- $result->save();
+ $results = $this->buildResults($validated['results']);
+ foreach ($results as $resultData) {
+ $cutting->results()->create($resultData);
}
$cutting->load('materials.rawMaterialPrice.rawMaterial');
@@ -576,8 +495,6 @@ function () use ($cutting, $validated): void {
if ($cutting->status === CuttingStatus::IN_PROGRESS) {
$this->reverseTotalMaterialStock($cutting);
- } elseif ($cutting->status === CuttingStatus::REJECTED) {
- $this->reverseMaterialStock($cutting);
}
$cutting->materials()->delete();
@@ -590,10 +507,6 @@ function () use ($cutting, $validated): void {
$cutting->description = $validated['description'] ?? null;
$cutting->sewing_cost = (int) ($validated['sewing_cost'] ?? 0);
$cutting->other_cost = (int) ($validated['other_cost'] ?? 0);
- if ($cutting->status === CuttingStatus::REJECTED) {
- $cutting->status = CuttingStatus::IN_PROGRESS;
- $cutting->rejection()?->delete();
- }
$cutting->save();
$this->syncImages($cutting, $validated);
@@ -660,9 +573,9 @@ function () use ($cutting, $validated): void {
public function delete(Cutting $cutting, User $user): void
{
- if (! in_array($cutting->status, [CuttingStatus::IN_PROGRESS, CuttingStatus::REJECTED], true)) {
+ if ($cutting->status !== CuttingStatus::IN_PROGRESS) {
throw ValidationException::withMessages([
- 'status' => 'Proses cutting hanya dapat dihapus saat masih proses atau ditolak.',
+ 'status' => 'Proses cutting hanya dapat dihapus saat masih proses.',
]);
}
@@ -672,11 +585,7 @@ public function delete(Cutting $cutting, User $user): void
function () use ($cutting): void {
$cutting->load(['materials.rawMaterialPrice.rawMaterial']);
- if ($cutting->status === CuttingStatus::IN_PROGRESS) {
- $this->reverseTotalMaterialStock($cutting);
- } elseif ($cutting->status === CuttingStatus::REJECTED) {
- $this->reverseMaterialStock($cutting);
- }
+ $this->reverseTotalMaterialStock($cutting);
$cutting->materials()->delete();
$cutting->results()->delete();
@@ -700,9 +609,6 @@ public function transitionStatus(
CuttingStatus $status,
User $user,
?string $reason = null,
- ?string $verificationNote = null,
- ?array $results = null,
- ?array $resultPrices = null,
): void {
if (! $cutting->status->canTransitionTo($status)) {
throw ValidationException::withMessages([
@@ -711,7 +617,7 @@ public function transitionStatus(
}
$this->runInTransaction(
- function () use ($cutting, $status, $verificationNote, $results, $resultPrices, $user, $reason): void {
+ function () use ($cutting, $status): void {
$cutting->load(['materials.rawMaterialPrice', 'results']);
if ($status === CuttingStatus::COMPLETED) {
@@ -719,37 +625,6 @@ function () use ($cutting, $status, $verificationNote, $results, $resultPrices,
$cutting->cost_per_unit = $this->calculateCostPerUnit($cutting);
}
- if ($status === CuttingStatus::IN_PROGRESS) {
- $cutting->rejection()?->delete();
- }
-
- if ($status === CuttingStatus::REJECTED) {
- $this->storeRejection($cutting, $reason, $user);
- }
-
- if ($status === CuttingStatus::VERIFIED) {
- if ($results !== null) {
- foreach ($results as $item) {
- $cutting->results()
- ->where('product_variant_id', $item['product_variant_id'])
- ->update([
- 'sample' => $item['sample'],
- 'original_outside_sample' => $item['original_outside_sample'],
- ]);
- }
- $cutting->load('results');
- }
- $this->applyProductStockOnVerify($cutting);
- $this->storeResultPrices($cutting, $resultPrices ?? []);
-
- if ($verificationNote !== null && trim($verificationNote) !== '') {
- $cutting->rejection()->create([
- 'reason' => trim($verificationNote),
- 'rejected_by_id' => $user->id,
- ]);
- }
- }
-
$cutting->status = $status;
$cutting->save();
},
@@ -758,41 +633,24 @@ function () use ($cutting, $status, $verificationNote, $results, $resultPrices,
$description = $cutting->description ?? '-';
$message = match ($status) {
- CuttingStatus::COMPLETED => "Proses cutting dengan deskripsi '{$description}' telah selesai oleh {$user->profile?->full_name} dan menunggu verifikasi.",
- CuttingStatus::VERIFIED => "Proses cutting dengan deskripsi '{$description}' telah diverifikasi oleh {$user->profile?->full_name}.",
- CuttingStatus::REJECTED => "Proses cutting dengan deskripsi '{$description}' ditolak oleh {$user->profile?->full_name}".($reason ? " dengan alasan: '{$reason}'" : '').'.',
+ CuttingStatus::COMPLETED => "Proses cutting dengan deskripsi '{$description}' telah selesai oleh {$user->profile?->full_name}.",
CuttingStatus::IN_PROGRESS => "Proses cutting dengan deskripsi '{$description}' dikembalikan ke proses oleh {$user->profile?->full_name}.",
default => "Status proses cutting dengan deskripsi '{$description}' telah diperbarui ke: {$status->label()} oleh {$user->profile?->full_name}.",
};
$title = match ($status) {
CuttingStatus::COMPLETED => '✂️ Proses Cutting Selesai',
- CuttingStatus::VERIFIED => '✂️ Proses Cutting Terverifikasi',
- CuttingStatus::REJECTED => '✂️ Proses Cutting Ditolak',
CuttingStatus::IN_PROGRESS => '✂️ Proses Cutting Dikembalikan',
default => '✂️ Proses Cutting Diperbarui',
};
- $roles = $status === CuttingStatus::COMPLETED
- ? ['owner', 'developer', 'direktur']
- : ['owner', 'developer', 'direktur'];
-
$this->pushNotificationService->sendToRoles(
$title,
$message,
- $roles,
+ ['owner', 'developer', 'direktur'],
route('admin.manage.cuttings.index'),
);
- if ($status === CuttingStatus::COMPLETED) {
- $this->pushNotificationService->sendToRoles(
- $title,
- $message,
- ['admin-toko'],
- route('admin.manage.stocks.index'),
- );
- }
-
$this->cacheForgetByPattern('manage:cuttings:*');
$this->cacheForgetByPattern('prices:*');
}
@@ -838,13 +696,7 @@ private function buildResults(array $results): array
{
return collect($results)
->map(function (array $itemData, int $index) {
- $variant = ProductVariant::query()->find($itemData['product_variant_id']);
-
- if ($variant === null) {
- throw ValidationException::withMessages([
- "results.{$index}.product_variant_id" => 'Varian produk tidak ditemukan.',
- ]);
- }
+ $productName = $itemData['product_name'] ?? null;
$cuttingResult = (int) $itemData['cutting_result'];
$sample = (int) $itemData['sample'];
@@ -875,7 +727,7 @@ private function buildResults(array $results): array
}
return [
- 'product_variant_id' => $variant->id,
+ 'product_name' => $productName,
'cutting_result' => $cuttingResult,
'sample' => $sample,
'original_outside_sample' => $originalOutsideSample,
@@ -915,65 +767,6 @@ private function reverseTotalMaterialStock(Cutting $cutting): void
}
}
- private function reverseMaterialStock(Cutting $cutting): void
- {
- foreach ($cutting->materials as $material) {
- RawMaterialPrice::query()
- ->whereKey($material->raw_material_price_id)
- ->increment('stock', (float) $material->material_usage);
- }
- }
-
- private function applyProductStockOnVerify(Cutting $cutting): void
- {
- foreach ($cutting->results as $result) {
- if ($result->sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->increment('stock', $result->sample);
- }
-
- if ($result->original_outside_sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->increment('reject_stock', $result->original_outside_sample);
- }
- }
- }
-
- private function reverseProductStock(Cutting $cutting): void
- {
- foreach ($cutting->results as $result) {
- if ($result->sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->decrement('stock', $result->sample);
- }
-
- if ($result->original_outside_sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->decrement('reject_stock', $result->original_outside_sample);
- }
- }
- }
-
- private function storeRejection(Cutting $cutting, ?string $reason, User $user): void
- {
- if ($reason === null || trim($reason) === '') {
- throw ValidationException::withMessages([
- 'reason' => 'Alasan penolakan wajib diisi.',
- ]);
- }
-
- $cutting->rejection()?->delete();
-
- $cutting->rejection()->create([
- 'reason' => trim($reason),
- 'rejected_by_id' => $user->id,
- ]);
- }
-
private function breakMaterialCircularReference(CuttingMaterial $material): void
{
$price = $material->rawMaterialPrice;
@@ -1056,17 +849,11 @@ private function presentDraftMaterial(CuttingMaterial $item): array
private function presentDraftResult(CuttingResult $item): array
{
- $variant = $item->productVariant;
-
return [
- 'product_variant_id' => $item->product_variant_id,
- 'product_name' => $variant?->product?->name ?? '',
- 'variant_name' => $variant?->name ?? '',
- 'stock' => $variant?->stock ?? 0,
+ 'product_name' => $item->product_name ?? '',
'cutting_result' => $item->cutting_result !== null ? (string) $item->cutting_result : null,
'sample' => $item->sample !== null ? (string) $item->sample : null,
'original_outside_sample' => $item->original_outside_sample !== null ? (string) $item->original_outside_sample : null,
- 'images' => $variant ? MediaPresenter::collection($variant, 'images') : [],
];
}
@@ -1101,14 +888,6 @@ private function filterActionsForUser(Cutting $cutting, User $user): array
{
return collect($cutting->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
- ->filter(function (array $action) use ($cutting, $user): bool {
- // Only owner/developer can verify directly from COMPLETED status
- if ($cutting->status === CuttingStatus::COMPLETED && $action['status'] === CuttingStatus::VERIFIED->value) {
- return $user->hasRole([Role::OWNER->value, Role::DEVELOPER->value]);
- }
-
- return true;
- })
->values()
->all();
}
@@ -1164,23 +943,6 @@ public function calculateCostPerUnit(Cutting $cutting): int
return (int) round($this->calculateTotalProductionCost($cutting) / $totalPieces);
}
- private function storeResultPrices(Cutting $cutting, array $resultPrices): void
- {
- $costPerUnit = (int) ($cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting));
-
- foreach ($resultPrices as $resultData) {
- foreach ($resultData['prices'] as $priceData) {
- CuttingResultPrice::query()->create([
- 'cutting_id' => $cutting->id,
- 'product_variant_id' => $resultData['product_variant_id'],
- 'price_type' => $priceData['type'],
- 'price' => (int) $priceData['price'],
- 'cost_per_unit' => $costPerUnit,
- ]);
- }
- }
- }
-
/**
* Quick-create a product with variants and prices, bypassing owner verification.
*/
diff --git a/app/Services/Manage/OwnerVerificationService.php b/app/Services/Manage/OwnerVerificationService.php
index 491aec9..8fbc5e5 100644
--- a/app/Services/Manage/OwnerVerificationService.php
+++ b/app/Services/Manage/OwnerVerificationService.php
@@ -2,16 +2,12 @@
namespace App\Services\Manage;
-use App\Enums\OwnerVerificationAction;
use App\Enums\OwnerVerificationStatus;
-use App\Enums\Permission;
-use App\Models\Cutting;
use App\Models\OwnerVerificationRequest;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Purchase;
use App\Models\RawMaterial;
-use App\Models\Restock;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Master\ProductService;
@@ -23,8 +19,6 @@
use App\Support\OwnerVerification\VerificationChangeFormatter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Pagination\LengthAwarePaginator as Paginator;
-use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
@@ -34,7 +28,6 @@ class OwnerVerificationService
use CachesQuery;
public function __construct(
- private readonly StockService $stockService,
private readonly ProductService $productService,
private readonly RawMaterialService $rawMaterialService,
private readonly PurchaseService $purchaseService,
@@ -60,44 +53,8 @@ public function paginateForIndex(
string $action = '',
): LengthAwarePaginator {
$perPage = 25;
- $page = Paginator::resolveCurrentPage();
- $includeCuttings = $this->shouldIncludeCuttings($user, $status, $subjectType, $action);
- $cuttingRows = $includeCuttings
- ? $this->pendingCuttingRows($user, $tableQuery)
- : collect();
- $cuttingCount = $cuttingRows->count();
-
- $requestsOnly = $action === OwnerVerificationAction::STOCK_VERIFY->value
- || $subjectType === Cutting::class;
$query = $this->buildVerificationRequestQuery($user, $tableQuery, $status, $subjectType, $action);
- $requestTotal = $requestsOnly ? 0 : (clone $query)->count();
- $total = $cuttingCount + $requestTotal;
-
- if ($requestsOnly) {
- $items = $cuttingRows->forPage($page, $perPage)->values();
-
- return $this->makePaginator($items, $total, $perPage, $page);
- }
-
- if ($cuttingCount > 0) {
- if ($page === 1) {
- $requestLimit = max(0, $perPage - $cuttingCount);
- $requestItems = $requestLimit > 0
- ? $query->take($requestLimit)->get()->map(fn (OwnerVerificationRequest $request) => $this->presentRequestRow($request))
- : collect();
- $items = $cuttingRows->concat($requestItems)->values();
- } else {
- $requestOffset = ($page - 1) * $perPage - $cuttingCount;
- $items = $query
- ->skip(max(0, $requestOffset))
- ->take($perPage)
- ->get()
- ->map(fn (OwnerVerificationRequest $request) => $this->presentRequestRow($request));
- }
-
- return $this->makePaginator($items, $total, $perPage, $page);
- }
return $query
->paginate($perPage)
@@ -105,52 +62,19 @@ public function paginateForIndex(
->through(fn (OwnerVerificationRequest $request) => $this->presentRequestRow($request));
}
- private function pendingCuttingRows(User $user, array $tableQuery): Collection
- {
- $rows = $this->stockService
- ->getPendingApprovalCuttings($user)
- ->map(fn (Cutting $cutting) => $this->presentCuttingRow($cutting));
-
- if ($tableQuery['search'] === '') {
- return $rows->values();
- }
-
- $search = mb_strtolower($tableQuery['search']);
-
- return $rows
- ->filter(function (array $row) use ($search): bool {
- foreach (['title', 'summary', 'submitted_by_name', 'action_label', 'subject_label', 'status_label'] as $field) {
- $value = mb_strtolower((string) ($row[$field] ?? ''));
-
- if ($value !== '' && str_contains($value, $search)) {
- return true;
- }
- }
-
- return false;
- })
- ->values();
- }
-
public function pendingCountForUser(User $user): int
{
return $this->cacheRemember("verification:pending_count:{$user->id}", 30, function () use ($user) {
- $requestCount = OwnerVerificationRequest::query()
+ return OwnerVerificationRequest::query()
->pending()
->visibleTo($user)
->count();
-
- if (! $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
- return $requestCount;
- }
-
- return $requestCount + Cutting::query()->pendingVerification()->count();
});
}
public function subjectTypeOptions(User $user): array
{
- $options = OwnerVerificationRequest::query()
+ return OwnerVerificationRequest::query()
->visibleTo($user)
->distinct()
->pluck('subject_type')
@@ -158,17 +82,7 @@ public function subjectTypeOptions(User $user): array
->map(fn (string $type) => [
'value' => $type,
'label' => ModelLabel::for($type),
- ]);
-
- if ($user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)
- && Cutting::query()->pendingVerification()->exists()) {
- $options->push([
- 'value' => Cutting::class,
- 'label' => ModelLabel::for(Cutting::class),
- ]);
- }
-
- return $options
+ ])
->unique('value')
->sortBy('label')
->values()
@@ -355,37 +269,6 @@ private function presentRequestRow(OwnerVerificationRequest $request): array
];
}
- private function presentCuttingRow(Cutting $cutting): array
- {
- $totalPieces = $cutting->results->sum('cutting_result');
-
- return [
- 'id' => $cutting->id,
- 'source' => 'cutting',
- 'subject_type' => Cutting::class,
- 'subject_label' => ModelLabel::for(Cutting::class),
- 'subject_id' => $cutting->id,
- 'action' => OwnerVerificationAction::STOCK_VERIFY->value,
- 'action_label' => OwnerVerificationAction::STOCK_VERIFY->label(),
- 'status' => OwnerVerificationStatus::PENDING->value,
- 'status_label' => OwnerVerificationStatus::PENDING->label(),
- 'title' => "Cutting #{$cutting->id}",
- 'summary' => $cutting->description ?? "Total hasil {$totalPieces} pcs",
- 'submitted_by_name' => $cutting->submittedBy?->profile?->full_name
- ?? $cutting->submittedBy?->username
- ?? '-',
- 'created_at' => $cutting->created_at?->toIso8601String(),
- 'created_at_formatted' => $cutting->created_at?->translatedFormat('l, d F Y H:i'),
- 'changes' => [],
- 'is_pending' => true,
- 'detail' => [
- 'results' => $cutting->results,
- 'result_prices' => $cutting->result_prices,
- 'total_result_pieces' => $cutting->total_result_pieces,
- ],
- ];
- }
-
private function buildVerificationRequestQuery(
User $user,
array $tableQuery,
@@ -393,10 +276,6 @@ private function buildVerificationRequestQuery(
string $subjectType,
string $action,
): Builder {
- if ($action === OwnerVerificationAction::STOCK_VERIFY->value || $subjectType === Cutting::class) {
- return OwnerVerificationRequest::query()->whereRaw('0 = 1');
- }
-
$query = OwnerVerificationRequest::query()
->visibleTo($user)
->with([
@@ -435,46 +314,6 @@ private function buildVerificationRequestQuery(
return $query;
}
- private function shouldIncludeCuttings(
- User $user,
- string $status,
- string $subjectType,
- string $action,
- ): bool {
- if (! $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) {
- return false;
- }
-
- if ($status !== '' && $status !== OwnerVerificationStatus::PENDING->value) {
- return false;
- }
-
- if ($subjectType !== '' && $subjectType !== Cutting::class) {
- return false;
- }
-
- if ($action !== '' && $action !== OwnerVerificationAction::STOCK_VERIFY->value) {
- return false;
- }
-
- return Cutting::query()->pendingVerification()->exists();
- }
-
- private function makePaginator(
- Collection $items,
- int $total,
- int $perPage,
- int $page,
- ): LengthAwarePaginator {
- return (new Paginator(
- $items,
- $total,
- $perPage,
- $page,
- ['path' => Paginator::resolveCurrentPath(), 'pageName' => 'page'],
- ))->withQueryString();
- }
-
private function requestTitle(OwnerVerificationRequest $request): string
{
if ($request->subject_type === MarketplaceSettings::class) {
diff --git a/app/Services/Manage/StockService.php b/app/Services/Manage/StockService.php
index ab7b2c5..5c46f8b 100644
--- a/app/Services/Manage/StockService.php
+++ b/app/Services/Manage/StockService.php
@@ -2,424 +2,4 @@
namespace App\Services\Manage;
-use App\Enums\CuttingStatus;
-use App\Enums\Permission;
-use App\Models\Cutting;
-use App\Models\CuttingMaterial;
-use App\Models\CuttingResult;
-use App\Models\CuttingResultPrice;
-use App\Models\ProductPrice;
-use App\Models\ProductVariant;
-use App\Models\User;
-use App\Services\Concerns\CachesQuery;
-use App\Services\System\PushNotificationService;
-use App\Support\Media\MediaPresenter;
-use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Log;
-use Illuminate\Validation\ValidationException;
-
-class StockService
-{
- use CachesQuery;
-
- public function __construct(
- private readonly PushNotificationService $pushNotificationService,
- ) {}
-
- public function getPendingVerificationCuttings(User $user): Collection
- {
- return Cutting::query()
- ->with([
- 'createdBy.profile',
- 'rejection.rejectedBy.profile',
- 'materials.rawMaterialPrice.rawMaterial:id,name,unit',
- 'materials.rawMaterialPrice.media',
- 'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
- ])
- ->completed()
- ->latest()
- ->get()
- ->each(function (Cutting $cutting): void {
- $this->appendCostPreview($cutting);
- $this->appendPhotos($cutting);
- $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
- });
- }
-
- public function getPendingApprovalCuttings(User $user): Collection
- {
- return Cutting::query()
- ->with([
- 'createdBy.profile',
- 'submittedBy.profile',
- 'rejection.rejectedBy.profile',
- 'materials.rawMaterialPrice.rawMaterial:id,name,unit',
- 'materials.rawMaterialPrice.media',
- 'materials.combination',
- 'results.productVariant.product:id,name',
- 'results.productVariant.media',
- 'resultPrices.productVariant:id,product_id,name',
- ])
- ->pendingVerification()
- ->latest()
- ->get()
- ->each(function (Cutting $cutting): void {
- $this->appendCostPreview($cutting);
- $this->appendPhotos($cutting);
- $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
- });
- }
-
- public function submitVerification(
- Cutting $cutting,
- User $user,
- ?string $verificationNote = null,
- ?array $results = null,
- ?array $resultPrices = null,
- ): void {
- if ($cutting->status !== CuttingStatus::COMPLETED) {
- throw ValidationException::withMessages([
- 'status' => 'Hanya cutting yang sudah selesai yang dapat diverifikasi.',
- ]);
- }
-
- $isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
-
- try {
- DB::transaction(function () use ($cutting, $user, $verificationNote, $results, $resultPrices, $isOwner): void {
- $cutting->load(['materials.rawMaterialPrice', 'results']);
-
- if ($results !== null) {
- foreach ($results as $item) {
- $cutting->results()
- ->where('product_variant_id', $item['product_variant_id'])
- ->update([
- 'sample' => $item['good'],
- 'original_outside_sample' => $item['reject'],
- ]);
- }
- $cutting->load('results');
- }
-
- $this->storeResultPrices($cutting, $resultPrices ?? []);
-
- if ($verificationNote !== null && trim($verificationNote) !== '') {
- $cutting->rejection()->create([
- 'reason' => trim($verificationNote),
- 'rejected_by_id' => $user->id,
- ]);
- }
-
- $cutting->submitted_by_id = $user->id;
-
- if ($isOwner) {
- $this->applyProductStockOnVerify($cutting);
- $this->applyResultPricesToProducts($cutting);
- $cutting->status = CuttingStatus::VERIFIED;
- } else {
- $cutting->status = CuttingStatus::PENDING_VERIFICATION;
- }
-
- $cutting->save();
- });
- } catch (ValidationException $e) {
- throw $e;
- } catch (\Throwable $e) {
- Log::error('Gagal mengajukan verifikasi stok: '.$e->getMessage(), [
- 'trace' => $e->getTraceAsString(),
- ]);
-
- throw ValidationException::withMessages([
- 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
- ]);
- }
-
- $description = $cutting->description ?? '-';
-
- if ($isOwner) {
- $this->pushNotificationService->sendToRoles(
- '📦 Stok Cutting Diverifikasi',
- "Cutting dengan deskripsi '{$description}' telah disetujui oleh {$user->profile?->full_name} dan stok produk telah ditambahkan ke toko.",
- ['owner', 'developer', 'direktur'],
- route('admin.manage.stocks.index'),
- );
- } else {
- $this->pushNotificationService->sendToRoles(
- '📦 Verifikasi Stok Menunggu Persetujuan',
- "Cutting dengan deskripsi '{$description}' telah diajukan verifikasi oleh {$user->profile?->full_name} dan menunggu persetujuan owner.",
- ['owner', 'developer', 'direktur'],
- route('admin.manage.stocks.index'),
- );
- }
-
- $this->cacheForgetByPattern('manage:stocks:*');
- $this->cacheForgetByPattern('prices:*');
- }
-
- public function approveVerification(
- Cutting $cutting,
- User $user,
- ?string $approvalNote = null,
- ): void {
- if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) {
- throw ValidationException::withMessages([
- 'status' => 'Hanya cutting yang menunggu verifikasi yang dapat disetujui.',
- ]);
- }
-
- try {
- DB::transaction(function () use ($cutting, $user, $approvalNote): void {
- $cutting->load(['materials.rawMaterialPrice', 'results', 'resultPrices']);
-
- $this->applyProductStockOnVerify($cutting);
- $this->applyResultPricesToProducts($cutting);
-
- if ($approvalNote !== null && trim($approvalNote) !== '') {
- $cutting->rejection()->create([
- 'reason' => trim($approvalNote),
- 'rejected_by_id' => $user->id,
- ]);
- }
-
- $cutting->status = CuttingStatus::VERIFIED;
- $cutting->save();
- });
- } catch (ValidationException $e) {
- throw $e;
- } catch (\Throwable $e) {
- Log::error('Gagal menyetujui verifikasi stok: '.$e->getMessage(), [
- 'trace' => $e->getTraceAsString(),
- ]);
-
- throw ValidationException::withMessages([
- 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
- ]);
- }
-
- $description = $cutting->description ?? '-';
-
- $this->pushNotificationService->sendToRoles(
- '📦 Stok Cutting Diverifikasi',
- "Cutting dengan deskripsi '{$description}' telah disetujui oleh {$user->profile?->full_name} dan stok produk telah ditambahkan ke toko.",
- ['owner', 'developer', 'direktur'],
- route('admin.manage.stocks.index'),
- );
-
- $this->cacheForgetByPattern('manage:stocks:*');
- $this->cacheForgetByPattern('prices:*');
- }
-
- public function rejectVerification(
- Cutting $cutting,
- User $user,
- string $reason,
- ): void {
- if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) {
- throw ValidationException::withMessages([
- 'status' => 'Hanya cutting yang menunggu verifikasi yang dapat ditolak.',
- ]);
- }
-
- try {
- DB::transaction(function () use ($cutting, $user, $reason): void {
- $cutting->rejection()->create([
- 'reason' => trim($reason),
- 'rejected_by_id' => $user->id,
- ]);
-
- $cutting->status = CuttingStatus::COMPLETED;
- $cutting->save();
- });
- } catch (ValidationException $e) {
- throw $e;
- } catch (\Throwable $e) {
- Log::error('Gagal menolak verifikasi stok: '.$e->getMessage(), [
- 'trace' => $e->getTraceAsString(),
- ]);
-
- throw ValidationException::withMessages([
- 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
- ]);
- }
-
- $description = $cutting->description ?? '-';
- $this->pushNotificationService->sendToRoles(
- '📦 Verifikasi Cutting Ditolak Owner',
- "Cutting dengan deskripsi '{$description}' ditolak oleh {$user->profile?->full_name} dengan alasan: '{$reason}'.",
- ['owner', 'developer', 'direktur'],
- route('admin.manage.stocks.index'),
- );
-
- if ($cutting->submitted_by_id) {
- $this->pushNotificationService->sendToUser(
- '📦 Verifikasi Cutting Ditolak Owner',
- "Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.",
- $cutting->submitted_by_id,
- route('admin.manage.stocks.index'),
- );
- }
-
- $this->cacheForgetByPattern('manage:stocks:*');
- }
-
- private function breakMaterialCircularReference(CuttingMaterial $material): void
- {
- $price = $material->rawMaterialPrice;
-
- // Always set these attributes regardless of price
- $material->setAttribute('material_result', $material->material_result);
- $material->setAttribute('material_result_input', $material->material_result);
- $material->setAttribute('combination_id', $material->combination_id);
- $material->setAttribute('combination_material_result', $material->combination?->material_result);
-
- if ($price) {
- $rawMaterial = $price->rawMaterial;
-
- $material->setAttribute('variant', $price->variant);
- $material->setAttribute('stock_input', $price->stock_input);
- $material->setAttribute('images', $price->getAttribute('images') ?? []);
-
- if ($rawMaterial) {
- $unitAbbreviation = $rawMaterial->unit->abbreviation();
- $price->setAttribute('unit_abbreviation', $unitAbbreviation);
- $material->setAttribute('unit_abbreviation', $unitAbbreviation);
- $material->setAttribute('unit', $rawMaterial->unit->value);
- $material->setAttribute('raw_material_id', $rawMaterial->id);
- $material->setAttribute('raw_material_name', $rawMaterial->name);
- $material->setAttribute('raw_material_unit_label', $rawMaterial->unit->label());
- }
-
- $price->unsetRelation('rawMaterial');
- }
-
- $material->unsetRelation('rawMaterialPrice');
- $material->unsetRelation('combination');
- }
-
- private function applyProductStockOnVerify(Cutting $cutting): void
- {
- foreach ($cutting->results as $result) {
- if ($result->sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->increment('stock', $result->sample);
- }
-
- if ($result->original_outside_sample > 0) {
- ProductVariant::query()
- ->whereKey($result->product_variant_id)
- ->increment('reject_stock', $result->original_outside_sample);
- }
- }
- }
-
- private function storeResultPrices(Cutting $cutting, array $resultPrices): void
- {
- // Extract harga_modal from the first variant's prices and set as cost_per_unit
- $costPerUnit = 0;
- foreach ($resultPrices as $resultData) {
- foreach ($resultData['prices'] as $priceData) {
- if ($priceData['type'] === 'harga_modal' && (int) $priceData['price'] > 0) {
- $costPerUnit = (int) $priceData['price'];
- break 2;
- }
- }
- }
-
- if ($costPerUnit > 0) {
- $cutting->cost_per_unit = $costPerUnit;
- } else {
- $costPerUnit = (int) ($cutting->cost_per_unit ?? 0);
- }
-
- foreach ($resultPrices as $resultData) {
- foreach ($resultData['prices'] as $priceData) {
- if ((int) $priceData['price'] > 0) {
- // Store/update to cutting_result_prices
- CuttingResultPrice::query()->updateOrCreate(
- [
- 'cutting_id' => $cutting->id,
- 'product_variant_id' => $resultData['product_variant_id'],
- 'price_type' => $priceData['type'],
- ],
- [
- 'price' => (int) $priceData['price'],
- 'cost_per_unit' => $costPerUnit,
- ],
- );
- }
- }
- }
- }
-
- private function applyResultPricesToProducts(Cutting $cutting): void
- {
- $resultPrices = $cutting->resultPrices()->with('productVariant')->get();
-
- foreach ($resultPrices as $resultPrice) {
- if ($resultPrice->price > 0) {
- ProductPrice::query()->updateOrCreate(
- [
- 'variant_id' => $resultPrice->product_variant_id,
- 'type' => $resultPrice->price_type->value,
- ],
- [
- 'price' => $resultPrice->price,
- ],
- );
- }
- }
- }
-
- private function appendCostPreview(Cutting $cutting): void
- {
- $totalResultPieces = (int) $cutting->results->sum('cutting_result');
- $cutting->setAttribute('total_result_pieces', $totalResultPieces);
- $cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum('material_usage'));
-
- $totalMaterialCost = $cutting->total_material_cost ?? 0;
- $sewingCost = (int) ($cutting->sewing_cost ?? 0);
- $otherCost = (int) ($cutting->other_cost ?? 0);
- $totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost;
- $costPerUnit = $cutting->cost_per_unit ?? 0;
- $materialCostPerProduct = $totalResultPieces > 0 ? (int) round($totalMaterialCost / $totalResultPieces) : 0;
-
- $cutting->setAttribute('total_material_cost', $totalMaterialCost);
- $cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.'));
- $cutting->setAttribute('material_cost_per_product', $materialCostPerProduct);
- $cutting->setAttribute('material_cost_per_product_formatted', 'Rp '.number_format($materialCostPerProduct, 0, ',', '.'));
- $cutting->setAttribute('sewing_cost', $sewingCost);
- $cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.'));
- $cutting->setAttribute('other_cost', $otherCost);
- $cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.'));
- $cutting->setAttribute('total_production_cost', $totalProductionCost);
- $cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.'));
- $cutting->setAttribute('estimated_cost_per_unit', $costPerUnit);
- $cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
- }
-
- private function appendPhotos(Cutting $cutting): void
- {
- $cutting->materials->each(function (CuttingMaterial $material): void {
- $price = $material->rawMaterialPrice;
-
- if ($price) {
- $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
- }
- });
-
- $cutting->results->each(function (CuttingResult $result): void {
- $variant = $result->productVariant;
-
- if ($variant) {
- $variant->setAttribute(
- 'images',
- MediaPresenter::collection($variant, 'images'),
- );
- }
- });
- }
-}
+class StockService {}
diff --git a/app/Support/ActivityLog/ModelLabel.php b/app/Support/ActivityLog/ModelLabel.php
index 2160c48..81c35b9 100644
--- a/app/Support/ActivityLog/ModelLabel.php
+++ b/app/Support/ActivityLog/ModelLabel.php
@@ -10,7 +10,6 @@
use App\Models\Cutting;
use App\Models\CuttingMaterial;
use App\Models\CuttingResult;
-use App\Models\CuttingResultPrice;
use App\Models\Employee;
use App\Models\EmployeeAdvance;
use App\Models\Expense;
@@ -49,7 +48,6 @@ class ModelLabel
Cutting::class => 'Cutting',
CuttingMaterial::class => 'Bahan Cutting',
CuttingResult::class => 'Hasil Cutting',
- CuttingResultPrice::class => 'Harga Hasil Cutting',
Employee::class => 'Pegawai',
EmployeeAdvance::class => 'Kasbon',
Expense::class => 'Pengeluaran',
diff --git a/database/factories/CuttingFactory.php b/database/factories/CuttingFactory.php
index e0c4b80..119410f 100644
--- a/database/factories/CuttingFactory.php
+++ b/database/factories/CuttingFactory.php
@@ -27,11 +27,4 @@ public function completed(): static
'status' => CuttingStatus::COMPLETED->value,
]);
}
-
- public function verified(): static
- {
- return $this->state(fn (array $attributes) => [
- 'status' => CuttingStatus::VERIFIED->value,
- ]);
- }
}
diff --git a/database/factories/CuttingResultFactory.php b/database/factories/CuttingResultFactory.php
index bc79ff4..57095ab 100644
--- a/database/factories/CuttingResultFactory.php
+++ b/database/factories/CuttingResultFactory.php
@@ -4,7 +4,6 @@
use App\Models\Cutting;
use App\Models\CuttingResult;
-use App\Models\ProductVariant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@@ -19,7 +18,7 @@ public function definition(): array
return [
'cutting_id' => Cutting::factory(),
- 'product_variant_id' => ProductVariant::factory(),
+ 'product_name' => fake()->words(2, true),
'cutting_result' => $cuttingResult,
'sample' => $cuttingResult - $originalOutsideSample,
'original_outside_sample' => $originalOutsideSample,
diff --git a/database/migrations/2026_07_18_100150_make_cutting_results_product_variant_nullable_add_product_name.php b/database/migrations/2026_07_18_100150_make_cutting_results_product_variant_nullable_add_product_name.php
new file mode 100644
index 0000000..30b3e96
--- /dev/null
+++ b/database/migrations/2026_07_18_100150_make_cutting_results_product_variant_nullable_add_product_name.php
@@ -0,0 +1,74 @@
+string('product_name', 255)->nullable()->after('id');
+ });
+
+ DB::statement('
+ UPDATE cutting_results cr
+ JOIN product_variants pv ON cr.product_variant_id = pv.id
+ JOIN products p ON pv.product_id = p.id
+ SET cr.product_name = p.name
+ WHERE cr.product_name IS NULL
+ AND cr.product_variant_id IS NOT NULL
+ ');
+ }
+
+ if (Schema::hasColumn('cutting_results', 'product_variant_id')) {
+ Schema::table('cutting_results', function (Blueprint $table) {
+ if ($this->hasForeignKey('cutting_results', 'product_variant_id')) {
+ $table->dropForeign(['product_variant_id']);
+ }
+ $table->dropColumn('product_variant_id');
+ });
+ }
+
+ if (Schema::hasColumn('cutting_results', 'variant_name')) {
+ Schema::table('cutting_results', function (Blueprint $table) {
+ $table->dropColumn('variant_name');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (! Schema::hasColumn('cutting_results', 'product_variant_id')) {
+ Schema::table('cutting_results', function (Blueprint $table) {
+ $table->foreignId('product_variant_id')->nullable()->after('cutting_id')->constrained()->restrictOnDelete();
+ });
+ }
+
+ if (Schema::hasColumn('cutting_results', 'product_name')) {
+ Schema::table('cutting_results', function (Blueprint $table) {
+ $table->dropColumn('product_name');
+ });
+ }
+
+ if (! Schema::hasColumn('cutting_results', 'variant_name')) {
+ Schema::table('cutting_results', function (Blueprint $table) {
+ $table->string('variant_name', 255)->nullable();
+ });
+ }
+ }
+};
diff --git a/database/migrations/2026_07_19_010000_drop_cutting_result_prices_table.php b/database/migrations/2026_07_19_010000_drop_cutting_result_prices_table.php
new file mode 100644
index 0000000..4a80934
--- /dev/null
+++ b/database/migrations/2026_07_19_010000_drop_cutting_result_prices_table.php
@@ -0,0 +1,26 @@
+id();
+ $table->foreignId('cutting_id')->constrained()->restrictOnDelete();
+ $table->foreignId('product_variant_id')->constrained()->restrictOnDelete();
+ $table->string('price_type');
+ $table->unsignedInteger('price')->default(0);
+ $table->unsignedInteger('cost_per_unit')->default(0);
+ $table->timestamps();
+ $table->unique(['cutting_id', 'product_variant_id', 'price_type'], 'cutting_result_prices_unique');
+ });
+ }
+};
diff --git a/database/seeders/CuttingResultSeeder.php b/database/seeders/CuttingResultSeeder.php
index 3be0a8d..8ad9d0b 100644
--- a/database/seeders/CuttingResultSeeder.php
+++ b/database/seeders/CuttingResultSeeder.php
@@ -4,7 +4,6 @@
use App\Models\Cutting;
use App\Models\CuttingResult;
-use App\Models\ProductVariant;
use Illuminate\Database\Seeder;
class CuttingResultSeeder extends Seeder
@@ -12,9 +11,8 @@ class CuttingResultSeeder extends Seeder
public function run(): void
{
$cutting = Cutting::query()->first();
- $variant = ProductVariant::query()->first();
- if ($cutting === null || $variant === null) {
+ if ($cutting === null) {
CuttingResult::factory()->count(3)->create();
return;
@@ -24,7 +22,6 @@ public function run(): void
->count(2)
->create([
'cutting_id' => $cutting->id,
- 'product_variant_id' => $variant->id,
]);
}
}
diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue
index 52cf7dd..ee7060f 100644
--- a/resources/js/components/AppSidebar.vue
+++ b/resources/js/components/AppSidebar.vue
@@ -25,7 +25,7 @@ interface MenuItem {
href: string;
icon: any;
permission?: string | string[];
- badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings';
+ badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances';
}
interface MenuGroup {
@@ -57,7 +57,6 @@ const menuGroups: MenuGroup[] = [
{ title: 'Belanja', href: admin.manage.purchases.index.url(), icon: ShoppingBag, permission: 'purchases.view' },
{ title: 'Cutting', href: admin.manage.cuttings.index.url(), icon: Scissors, permission: 'cuttings.view' },
{ title: 'Restock', href: admin.manage.restocks.index.url(), icon: PackagePlus, permission: 'restocks.view' },
- { title: 'Stok Gudang', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' },
{ title: 'Stok Opname', href: admin.manage.stokOpnames.index.url(), icon: ClipboardList, permission: 'stok_opnames.view' },
{ title: 'Pesanan', href: admin.manage.orders.index.url(), icon: ShoppingCart, permission: 'orders.view' },
],
diff --git a/resources/js/components/MobileBottomNav.vue b/resources/js/components/MobileBottomNav.vue
index 1144413..74376a1 100644
--- a/resources/js/components/MobileBottomNav.vue
+++ b/resources/js/components/MobileBottomNav.vue
@@ -13,7 +13,7 @@ interface NavItem {
href: string;
icon: any;
permission?: string;
- badgeKey?: 'pendingLeaveRequests' | 'pendingCuttings';
+ badgeKey?: 'pendingLeaveRequests';
}
const navItems: NavItem[] = [
diff --git a/resources/js/components/owner-verification/OwnerVerificationRowActions.vue b/resources/js/components/owner-verification/OwnerVerificationRowActions.vue
index 08eedb7..91e1141 100644
--- a/resources/js/components/owner-verification/OwnerVerificationRowActions.vue
+++ b/resources/js/components/owner-verification/OwnerVerificationRowActions.vue
@@ -19,10 +19,10 @@ import {
} from '@/components/ui/field';
import { Textarea } from '@/components/ui/textarea';
import { useCan } from '@/composables/useCan';
-import { approve, reject, approve_request, reject_request } from '@/routes/admin/manage/owner_verifications';
+import { approve_request, reject_request } from '@/routes/admin/manage/owner_verifications';
const props = defineProps<{
- type: 'request' | 'cutting';
+ type: 'request';
id: number;
}>();
@@ -35,24 +35,15 @@ const rejectForm = useForm({
reason: '',
});
-const approveUrl = computed(() => (
- props.type === 'cutting'
- ? approve.url(props.id)
- : approve_request.url(props.id)
-));
-
-const rejectUrl = computed(() => (
- props.type === 'cutting'
- ? reject.url(props.id)
- : reject_request.url(props.id)
-));
+const approveUrl = approve_request.url(props.id);
+const rejectUrl = reject_request.url(props.id);
const canApprove = computed(() => can('owner_verifications.verify'));
const canReject = computed(() => can('owner_verifications.reject'));
function submitApprove() {
approveForm
- .post(approveUrl.value, {
+ .post(approveUrl, {
preserveScroll: true,
onError: (errors: Record) => {
const firstError = Object.values(errors)[0];
@@ -66,7 +57,7 @@ function submitApprove() {
function submitReject() {
rejectForm
- .post(rejectUrl.value, {
+ .post(rejectUrl, {
preserveScroll: true,
onSuccess: () => {
rejectDialogOpen.value = false;
diff --git a/resources/js/constants/cutting-status.ts b/resources/js/constants/cutting-status.ts
index 5f0320b..79479cd 100644
--- a/resources/js/constants/cutting-status.ts
+++ b/resources/js/constants/cutting-status.ts
@@ -1,26 +1,14 @@
-import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
-import type { Component } from 'vue';
+import { Scissors } from '@lucide/vue';
import type { BadgeVariant } from '@/lib/badge-variant';
export const CuttingStatus = {
IN_PROGRESS: 'in_progress',
COMPLETED: 'completed',
- PENDING_VERIFICATION: 'pending_verification',
- VERIFIED: 'verified',
- REJECTED: 'rejected',
} as const;
export type CuttingStatusValue = (typeof CuttingStatus)[keyof typeof CuttingStatus];
export function cuttingStatusBadgeVariant(status: string): BadgeVariant {
- if (status === CuttingStatus.VERIFIED) {
- return 'default';
- }
-
- if (status === CuttingStatus.REJECTED) {
- return 'destructive';
- }
-
if (status === CuttingStatus.COMPLETED) {
return 'secondary';
}
@@ -30,32 +18,12 @@ export function cuttingStatusBadgeVariant(status: string): BadgeVariant {
export function cuttingStatusTransitionConfirmDescription(targetStatus: string): string {
if (targetStatus === CuttingStatus.COMPLETED) {
- return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong sesuai pemakaian. Menunggu verifikasi admin toko.';
- }
-
- if (targetStatus === CuttingStatus.VERIFIED) {
- return 'Hasil cutting akan diverifikasi. Stok Sample dan hasil cutting Diluar Sample akan ditambahkan ke produk.';
- }
-
- if (targetStatus === CuttingStatus.IN_PROGRESS) {
- return 'Cutting dikembalikan ke proses untuk diperbaiki.';
+ return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong sesuai pemakaian.';
}
return '';
}
-export function cuttingStatusActionIcon(status: string): Component {
- if (status === CuttingStatus.COMPLETED) {
- return Scissors;
- }
-
- if (status === CuttingStatus.VERIFIED) {
- return Check;
- }
-
- if (status === CuttingStatus.IN_PROGRESS) {
- return RotateCcw;
- }
-
- return X;
+export function cuttingStatusActionIcon(status: string): any {
+ return Scissors;
}
diff --git a/resources/js/pages/admin/manage/cuttings/Create.vue b/resources/js/pages/admin/manage/cuttings/Create.vue
index 2c47aef..42c3499 100644
--- a/resources/js/pages/admin/manage/cuttings/Create.vue
+++ b/resources/js/pages/admin/manage/cuttings/Create.vue
@@ -5,20 +5,16 @@ import AdminLayout from '@/layouts/AdminLayout.vue';
import { index, store } from '@/routes/admin/manage/cuttings';
import type {
CuttingMaterialCartItem,
- CuttingProductCatalogItem,
CuttingRawMaterialCatalogItem,
CuttingResultCartItem,
} from '@/types/cutting';
-import type { CategoryOption } from '@/types/product';
import type { EnumOption } from '@/types/raw-material';
import CuttingPosForm from './form/CuttingPosForm.vue';
defineProps<{
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
- productCatalog: CuttingProductCatalogItem[];
draftMaterials: CuttingMaterialCartItem[];
draftResults: CuttingResultCartItem[];
- categories: CategoryOption[];
units: EnumOption[];
}>();
@@ -41,10 +37,8 @@ defineProps<{
();
@@ -39,15 +35,12 @@ const initialData = computed(() => ({
combination_material_result: item.combination_material_result ?? null,
})),
results: props.cutting.results.map((item) => ({
- product_variant_id: item.product_variant_id,
- product_name: item.product_variant?.product?.name ?? '',
- variant_name: item.product_variant?.name ?? '',
- stock: item.product_variant?.stock ?? 0,
+ product_name: item.product_name ?? '',
cutting_result: String(item.cutting_result ?? 0),
sample: String(item.sample ?? 0),
original_outside_sample: String(item.original_outside_sample ?? 0),
- images: item.product_variant?.images ?? [],
})),
+ images: props.cutting.images ?? [],
}));
@@ -64,8 +57,8 @@ const initialData = computed(() => ({
-
diff --git a/resources/js/pages/admin/manage/cuttings/Share.vue b/resources/js/pages/admin/manage/cuttings/Share.vue
index 11316d3..86767d1 100644
--- a/resources/js/pages/admin/manage/cuttings/Share.vue
+++ b/resources/js/pages/admin/manage/cuttings/Share.vue
@@ -25,12 +25,6 @@ interface GroupedMaterials {
isCombination?: boolean;
}
-interface GroupedResults {
- key: number;
- name: string;
- items: typeof props.cutting.results;
-}
-
const groupedMaterials = computed(() => {
// First, group by combination_id
const combinationGroups: Record = {};
@@ -83,24 +77,6 @@ const groupedMaterials = computed(() => {
return result;
});
-
-const groupedResults = computed(() => {
- const groups: Record = {};
-
- props.cutting.results.forEach((item) => {
- const product = item.product_variant?.product;
- const key = product?.id ?? 0;
- const name = product?.name ?? 'Produk Tidak Diketahui';
-
- if (!groups[key]) {
- groups[key] = { key, name, items: [] };
- }
-
- groups[key].items.push(item);
- });
-
- return Object.values(groups);
-});
@@ -144,6 +120,13 @@ const groupedResults = computed(() => {
{{ cutting.description }}
+
+
+
+
@@ -153,8 +136,7 @@ const groupedResults = computed(() => {
- Varian
- Foto
+ Produk
Hasil
@@ -167,55 +149,30 @@ const groupedResults = computed(() => {
-
-
+ {{ result.product_name ?? 'Produk Tidak Diketahui' }}
+
+
-
- {{ groupIndex + 1 }}. {{ group.name }}
-
-
-
+
-
- {{ result.product_variant?.name }}
-
-
-
-
-
- {{ result.cutting_result }} pcs
-
-
- {{ result.sample }} pcs
-
-
- {{ result.original_outside_sample }}
- pcs
-
-
-
+ {{ result.sample }} pcs
+
+
+ {{ result.original_outside_sample }}
+ pcs
+
+
diff --git a/resources/js/pages/admin/manage/cuttings/form/CuttingPosCartDetailDialog.vue b/resources/js/pages/admin/manage/cuttings/form/CuttingPosCartDetailDialog.vue
index df4bf6f..31363b6 100644
--- a/resources/js/pages/admin/manage/cuttings/form/CuttingPosCartDetailDialog.vue
+++ b/resources/js/pages/admin/manage/cuttings/form/CuttingPosCartDetailDialog.vue
@@ -27,9 +27,6 @@ const emit = defineEmits<{
'sync-material-field': [index: number];
'remove-combination': [combinationId: number];
'sync-combination-result': [combinationId: number, result: number | null];
- 'remove-result': [index: number];
- 'sync-result-totals': [item: CuttingResultCartItem];
- 'sync-result-field': [index: number];
}>();
const open = defineModel('open', { required: true });
@@ -53,9 +50,7 @@ const open = defineModel('open', { required: true });
Hasil Produk
+ :total-result-pieces="totalResultPieces" :is-create-mode="isCreateMode" />
diff --git a/resources/js/pages/admin/manage/cuttings/form/CuttingPosForm.vue b/resources/js/pages/admin/manage/cuttings/form/CuttingPosForm.vue
index 1084cc4..85174b3 100644
--- a/resources/js/pages/admin/manage/cuttings/form/CuttingPosForm.vue
+++ b/resources/js/pages/admin/manage/cuttings/form/CuttingPosForm.vue
@@ -6,24 +6,20 @@ import { toast } from 'vue-sonner';
import { parseRupiah } from '@/lib/rupiah';
import type {
CuttingMaterialCartItem,
- CuttingProductCatalogItem,
CuttingRawMaterialCatalogItem,
CuttingResultCartItem,
} from '@/types/cutting';
-import type { MediaItem } from '@/types/media';
-import type { CategoryOption } from '@/types/product';
+import type { MediaItem, MediaUploadState } from '@/types/media';
+import { createMediaUploadState, appendRootPhotosToFormData } from '@/types/media';
import type { EnumOption } from '@/types/raw-material';
import CuttingPosCartDetailDialog from './CuttingPosCartDetailDialog.vue';
import CuttingPosMaterialCatalogPanel from './CuttingPosMaterialCatalogPanel.vue';
-import CuttingPosResultCatalogPanel from './CuttingPosResultCatalogPanel.vue';
import CuttingPosSummaryPanel from './CuttingPosSummaryPanel.vue';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { useCuttingPosCart } from './useCuttingPosCart';
const props = defineProps<{
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
- productCatalog: CuttingProductCatalogItem[];
- categories: CategoryOption[];
units: EnumOption[];
initialData?: {
description: string;
@@ -42,16 +38,12 @@ const props = defineProps<{
const isCreateMode = computed(() => props.method === 'post');
const cartDetailOpen = ref(false);
-
-// No image upload state needed
+const photoState = ref
(createMediaUploadState());
// Make catalogs mutable so we can add new items from quick-create dialogs
const rawMaterialCatalogState = ref([
...props.rawMaterialCatalog,
]);
-const productCatalogState = ref([
- ...props.productCatalog,
-]);
watch(
() => props.rawMaterialCatalog,
@@ -59,12 +51,6 @@ watch(
rawMaterialCatalogState.value = [...val];
},
);
-watch(
- () => props.productCatalog,
- (val) => {
- productCatalogState.value = [...val];
- },
-);
const form = useForm({
description: '',
@@ -75,38 +61,29 @@ const form = useForm({
const {
materialSearch,
selectedRawMaterialId,
- productSearch,
materialCart,
resultCart,
filteredRawMaterials,
- filteredProducts,
totalMaterialCost,
totalResultPieces,
materialLineCost,
setCarts,
loadDraftItems,
getMaterialCartItem,
- getResultCartItem,
- syncResultTotals,
addMaterial,
removeMaterial,
decreaseMaterialQty,
- addResult,
- removeResult,
- decreaseResultQty,
syncMaterialField,
- syncResultField,
removeCombination,
syncCombinationResult,
} = useCuttingPosCart({
rawMaterialCatalog: rawMaterialCatalogState,
- productCatalog: productCatalogState,
isCreateMode: () => isCreateMode.value,
});
// Confirmation Dialog states
const showDeleteConfirm = ref(false);
-const deleteType = ref<'material' | 'result' | 'combination' | null>(null);
+const deleteType = ref<'material' | 'combination' | null>(null);
const deleteIndex = ref(null);
const deleteCombinationId = ref(null);
@@ -114,20 +91,14 @@ const deleteConfirmTitle = computed(() => {
if (deleteType.value === 'combination') {
return 'Hapus Kombinasi?';
}
- if (deleteType.value === 'material') {
- return 'Hapus Bahan Baku?';
- }
- return 'Hapus Hasil Produk?';
+ return 'Hapus Bahan Baku?';
});
const deleteConfirmDescription = computed(() => {
if (deleteType.value === 'combination') {
return 'Apakah Anda yakin ingin menghapus kombinasi bahan baku ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
}
- if (deleteType.value === 'material') {
- return 'Apakah Anda yakin ingin menghapus bahan baku ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
- }
- return 'Apakah Anda yakin ingin menghapus hasil produk ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
+ return 'Apakah Anda yakin ingin menghapus bahan baku ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
});
function confirmRemoveMaterial(index: number) {
@@ -136,12 +107,6 @@ function confirmRemoveMaterial(index: number) {
showDeleteConfirm.value = true;
}
-function confirmRemoveResult(index: number) {
- deleteType.value = 'result';
- deleteIndex.value = index;
- showDeleteConfirm.value = true;
-}
-
function confirmRemoveCombination(combinationId: number) {
deleteType.value = 'combination';
deleteCombinationId.value = combinationId;
@@ -151,8 +116,6 @@ function confirmRemoveCombination(combinationId: number) {
async function handleConfirmDelete() {
if (deleteType.value === 'material' && deleteIndex.value !== null) {
await removeMaterial(deleteIndex.value);
- } else if (deleteType.value === 'result' && deleteIndex.value !== null) {
- await removeResult(deleteIndex.value);
} else if (deleteType.value === 'combination' && deleteCombinationId.value !== null) {
await removeCombination(deleteCombinationId.value);
}
@@ -171,11 +134,21 @@ function populateForm() {
form.description = props.initialData.description;
form.sewing_cost = props.initialData.sewing_cost ?? '0';
form.other_cost = props.initialData.other_cost ?? '0';
+ photoState.value = createMediaUploadState(props.initialData.images ?? []);
setCarts(props.initialData.materials, props.initialData.results);
}
watch(() => props.initialData, populateForm, { immediate: true });
-loadDraftItems(props.draftMaterials ?? [], props.draftResults ?? []);
+loadDraftItems(props.draftMaterials ?? []);
+
+if (isCreateMode.value && resultCart.value.length === 0) {
+ resultCart.value.push({
+ product_name: '',
+ cutting_result: '',
+ sample: '',
+ original_outside_sample: '',
+ });
+}
function buildFormData(): FormData {
const formData = new FormData();
@@ -194,10 +167,9 @@ function buildFormData(): FormData {
String(Number.parseInt(parseRupiah(form.other_cost), 10) || 0),
);
- // No images to append
+ appendRootPhotosToFormData(formData, photoState.value);
if (props.method === 'put') {
- // Build combination results map
const combinationResults: Record = {};
materialCart.value.forEach(item => {
if (item.combination_id && item.combination_material_result !== undefined) {
@@ -228,7 +200,6 @@ function buildFormData(): FormData {
String(item.combination_id),
);
- // Add combination_material_result
const combinationResult = combinationResults[item.combination_id];
if (combinationResult !== undefined && combinationResult !== null) {
@@ -239,23 +210,27 @@ function buildFormData(): FormData {
}
}
});
- resultCart.value.forEach((item, index) => {
- formData.append(
- `results[${index}][product_variant_id]`,
- String(item.product_variant_id),
- );
- formData.append(
- `results[${index}][cutting_result]`,
- String(item.cutting_result),
- );
- formData.append(`results[${index}][sample]`, String(item.sample));
- formData.append(
- `results[${index}][original_outside_sample]`,
- String(item.original_outside_sample),
- );
- });
}
+ resultCart.value.forEach((item, index) => {
+ formData.append(
+ `results[${index}][product_name]`,
+ item.product_name ?? '',
+ );
+ formData.append(
+ `results[${index}][cutting_result]`,
+ item.cutting_result === '' ? '' : String(item.cutting_result ?? 0),
+ );
+ formData.append(
+ `results[${index}][sample]`,
+ item.sample === '' ? '' : String(item.sample ?? 0),
+ );
+ formData.append(
+ `results[${index}][original_outside_sample]`,
+ item.original_outside_sample === '' ? '' : String(item.original_outside_sample ?? 0),
+ );
+ });
+
return formData;
}
@@ -296,32 +271,14 @@ function submit() {
function onCombinationCreated(items: CuttingMaterialCartItem[]) {
for (const item of items) {
- // Combination items use raw_material_price_id + combination_id as unique key
- // Same variant can exist in multiple combinations
materialCart.value.push({ ...item });
}
}
-
-function onProductCreated(product: CuttingProductCatalogItem) {
- // Add to catalog
- productCatalogState.value.push(product);
-
- // Auto-add the first variant to cart
- const firstVariant = product.variants[0];
-
- if (firstVariant) {
- addResult(product, firstVariant);
- }
-}
-
-
@@ -345,10 +301,7 @@ function onProductCreated(product: CuttingProductCatalogItem) {
@remove-material="confirmRemoveMaterial"
@sync-material-field="syncMaterialField"
@remove-combination="confirmRemoveCombination"
- @sync-combination-result="syncCombinationResult"
- @remove-result="confirmRemoveResult"
- @sync-result-totals="syncResultTotals"
- @sync-result-field="syncResultField" />
+ @sync-combination-result="syncCombinationResult" />
+
+
Catatan: {{ cutting.description }}
@@ -162,7 +165,7 @@ const totalCompleted = computed(() => props.cuttings.length);
Hasil Produk:
-
- {{ res.product_variant?.product?.name }} ({{ res.product_variant?.name }}) - {{ res.cutting_result }} pcs
+ {{ res.product_name }} - {{ res.cutting_result }} pcs
- Belum ada hasil produk
diff --git a/resources/js/pages/admin/manage/cuttings/table/CuttingGroupedTable.vue b/resources/js/pages/admin/manage/cuttings/table/CuttingGroupedTable.vue
index 23fe390..fffd025 100644
--- a/resources/js/pages/admin/manage/cuttings/table/CuttingGroupedTable.vue
+++ b/resources/js/pages/admin/manage/cuttings/table/CuttingGroupedTable.vue
@@ -55,12 +55,6 @@ interface GroupedCuttingMaterials {
isCombination?: boolean;
}
-interface GroupedCuttingResults {
- productId: number;
- productName: string;
- items: any[];
-}
-
function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
// First, group by combination_id
const combinationGroups: Record
= {};
@@ -120,28 +114,6 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
return result;
}
-
-function getGroupedResults(results: any[]): GroupedCuttingResults[] {
- const groups: Record = {};
-
- results.forEach((item) => {
- const product = item.product_variant?.product;
- const productId = product?.id ?? 0;
- const productName = product?.name ?? 'Produk Tidak Diketahui';
-
- if (!groups[productId]) {
- groups[productId] = {
- productId,
- productName,
- items: [],
- };
- }
-
- groups[productId].items.push(item);
- });
-
- return Object.values(groups);
-}
@@ -183,6 +155,8 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
{{ cutting.description }}
+
+
Total Hasil Cutting
{{
@@ -296,48 +270,32 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
- Varian
- Foto
- Hasil
- Sample
- Diluar Sample
+ Produk
+ Hasil
+ Sample
+ Diluar Sample
-
+
Belum ada hasil produk
-
-
-
- {{ groupIndex + 1 }}. {{ group.productName }}
-
-
-
-
- {{ result.product_variant?.name }}
-
-
-
-
-
- {{ result.cutting_result }} pcs
-
-
- {{ result.sample }} pcs
-
-
- {{
- result.original_outside_sample
- }}
- pcs
-
-
-
+
+
+ {{ result.product_name ?? 'Produk Tidak Diketahui' }}
+
+
+ {{ result.cutting_result }} pcs
+
+
+ {{ result.sample }} pcs
+
+
+ {{ result.original_outside_sample }} pcs
+
+
diff --git a/resources/js/pages/admin/manage/cuttings/table/CuttingInProgressSection.vue b/resources/js/pages/admin/manage/cuttings/table/CuttingInProgressSection.vue
index 4dcf910..c7e5e3f 100644
--- a/resources/js/pages/admin/manage/cuttings/table/CuttingInProgressSection.vue
+++ b/resources/js/pages/admin/manage/cuttings/table/CuttingInProgressSection.vue
@@ -1,5 +1,6 @@
-
-
-
-
diff --git a/resources/js/pages/admin/manage/cuttings/table/CuttingVerifyDialog.vue b/resources/js/pages/admin/manage/cuttings/table/CuttingVerifyDialog.vue
deleted file mode 100644
index 748df78..0000000
--- a/resources/js/pages/admin/manage/cuttings/table/CuttingVerifyDialog.vue
+++ /dev/null
@@ -1,241 +0,0 @@
-
-
-
-
-
diff --git a/resources/js/pages/admin/manage/cuttings/table/data-table-actions.vue b/resources/js/pages/admin/manage/cuttings/table/data-table-actions.vue
index a32d9e8..70d1d12 100644
--- a/resources/js/pages/admin/manage/cuttings/table/data-table-actions.vue
+++ b/resources/js/pages/admin/manage/cuttings/table/data-table-actions.vue
@@ -4,7 +4,6 @@ import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import { RowDeleteAction, RowEditAction, RowShareAction, RowStatusAction } from '@/components/button';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
-import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
import { useCan } from '@/composables/useCan';
import {
cuttingStatusActionIcon,
@@ -13,8 +12,6 @@ import {
} from '@/constants/cutting-status';
import { edit, destroy, transition_status } from '@/routes/admin/manage/cuttings';
import type { CuttingListItem, CuttingStatusAction } from '@/types/cutting';
-import CuttingRejectDialog from './CuttingRejectDialog.vue';
-import CuttingVerifyDialog from './CuttingVerifyDialog.vue';
const props = defineProps<{
cutting: CuttingListItem;
@@ -24,8 +21,6 @@ const { can } = useCan();
const statusConfirmOpen = ref(false);
const statusProcessing = ref(false);
-const rejectDialogOpen = ref(false);
-const verifyDialogOpen = ref(false);
const pendingAction = ref(null);
const availableActions = computed(() => props.cutting.available_actions ?? []);
@@ -34,7 +29,7 @@ const canEdit = computed(
() => props.cutting.is_editable && can('cuttings.update'),
);
const canDelete = computed(
- () => can('cuttings.delete') && (props.cutting.status === CuttingStatus.IN_PROGRESS || props.cutting.status === CuttingStatus.REJECTED),
+ () => can('cuttings.delete') && (props.cutting.status === CuttingStatus.IN_PROGRESS),
);
function canPerformAction(action: CuttingStatusAction): boolean {
@@ -46,18 +41,6 @@ function statusConfirmDescription(action: CuttingStatusAction): string {
}
function openStatusConfirm(action: CuttingStatusAction) {
- if (action.status === CuttingStatus.VERIFIED) {
- verifyDialogOpen.value = true;
-
- return;
- }
-
- if (action.status === CuttingStatus.REJECTED) {
- rejectDialogOpen.value = true;
-
- return;
- }
-
pendingAction.value = action;
statusConfirmOpen.value = true;
}
@@ -96,9 +79,6 @@ function transitionStatus() {
-
-
@@ -120,8 +100,4 @@ function transitionStatus() {
" :description="pendingAction ? statusConfirmDescription(pendingAction) : ''
" :confirm-label="pendingAction?.label ?? 'Konfirmasi'" cancel-label="Batal"
:destructive="pendingAction?.destructive ?? false" :loading="statusProcessing" @confirm="transitionStatus" />
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/Index.vue b/resources/js/pages/admin/manage/stocks/Index.vue
deleted file mode 100644
index 044f529..0000000
--- a/resources/js/pages/admin/manage/stocks/Index.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
- Verifikasi Stok
-
-
-
-
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/table/StockPendingApprovalSection.vue b/resources/js/pages/admin/manage/stocks/table/StockPendingApprovalSection.vue
deleted file mode 100644
index f5aea86..0000000
--- a/resources/js/pages/admin/manage/stocks/table/StockPendingApprovalSection.vue
+++ /dev/null
@@ -1,246 +0,0 @@
-
-
-
-
-
-
-
-
-
- Cutting #{{ cutting.id }}
-
- Menunggu Persetujuan
-
-
-
{{ cutting.created_at_formatted }}
-
- Diajukan oleh {{ cutting.submitted_by.profile?.full_name ?? cutting.submitted_by.username }}
-
-
- Catatan: {{ cutting.rejection.reason }}
-
-
-
- Total Hasil Cutting {{ cutting.total_result_pieces ??
- 0 }} pcs
-
-
-
-
-
-
-
-
-
-
-
-
Bahan Baku
-
-
-
-
- Varian
- Foto
- Hasil
-
-
-
-
-
-
-
- {{ group.rawMaterialName }}
-
- {{ group.items.length }} bahan
-
-
- Hasil: {{ group.items[0].combination_material_result }} pcs
-
-
-
- {{ group.rawMaterialName }}
-
- {{ group.unitLabel }}
-
-
-
-
-
-
- {{ material.variant }}
-
-
-
-
-
-
- {{ material.material_result }} pcs
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
Hasil Verifikasi
-
-
-
-
- Varian
- Foto
- Hasil
- Bagus
- Reject
- Harga
-
-
-
-
-
- Belum ada hasil verifikasi
-
-
-
-
- {{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
-
-
-
-
-
- {{ result.cutting_result }} pcs
-
-
- {{ result.sample }} pcs
-
-
- {{ result.original_outside_sample }} pcs
-
-
-
-
-
- {{ price.type_label }}
-
-
- {{ price.price_formatted }}
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Tidak ada verifikasi yang menunggu persetujuan.
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/table/StockPendingSection.vue b/resources/js/pages/admin/manage/stocks/table/StockPendingSection.vue
deleted file mode 100644
index c5f82af..0000000
--- a/resources/js/pages/admin/manage/stocks/table/StockPendingSection.vue
+++ /dev/null
@@ -1,220 +0,0 @@
-
-
-
-
-
-
-
-
-
- Cutting #{{ cutting.id }}
-
-
-
-
{{ cutting.created_at_formatted }}
-
-
- Total Hasil Cutting {{ cutting.total_result_pieces ??
- 0 }} pcs
-
-
-
-
-
-
-
-
-
-
-
Hasil Produk
-
-
-
-
- Varian
- Foto
- Hasil
- Sample
- Diluar Sample
-
-
-
-
-
- Belum ada hasil produk
-
-
-
-
- {{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
-
-
-
-
-
- {{ result.cutting_result }} pcs
-
-
- {{ result.sample }} pcs
-
-
- {{ result.original_outside_sample }} pcs
-
-
-
-
-
-
-
-
-
Bahan Baku
-
-
-
-
- Varian
- Foto
- Hasil
-
-
-
-
-
-
-
- {{ group.rawMaterialName }}
-
- {{ group.items.length }} bahan
-
-
- Hasil: {{ group.items[0].combination_material_result }} pcs
-
-
-
- {{ group.rawMaterialName }}
-
- {{ group.unitLabel }}
-
-
-
-
-
-
- {{ material.variant }}
-
-
-
-
-
-
- {{ material.material_result }} pcs
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Tidak ada cutting yang menunggu verifikasi.
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/table/StockVerifiedSection.vue b/resources/js/pages/admin/manage/stocks/table/StockVerifiedSection.vue
deleted file mode 100644
index 034a13a..0000000
--- a/resources/js/pages/admin/manage/stocks/table/StockVerifiedSection.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
- Riwayat Verifikasi
-
-
-
-
-
-
-
-
-
-
- Cutting #{{ cutting.id }}
-
-
- Terverifikasi
-
-
-
-
{{ cutting.created_at_formatted }}
-
Oleh {{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}
-
-
- {{ cutting.description }}
-
-
- Total Hasil Cutting {{ cutting.total_result_pieces ??
- 0 }} pcs
- Biaya Produksi {{
- cutting.total_production_cost_formatted ?? 'Rp 0' }}
-
-
-
-
-
-
-
-
Hasil Produk
-
-
-
-
- Varian
- Foto
- Hasil
- Sample
- Diluar Sample
-
-
-
-
-
- Belum ada hasil produk
-
-
-
-
-
- {{ group.productName }}
-
-
-
-
- {{ result.product_variant?.name }}
-
-
-
-
-
- {{ result.cutting_result }} pcs
-
-
- {{ result.sample }} pcs
-
-
- {{ result.original_outside_sample }} pcs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Belum ada riwayat
-
- Belum ada cutting yang telah diverifikasi.
-
-
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/table/StockVerifyDialog.vue b/resources/js/pages/admin/manage/stocks/table/StockVerifyDialog.vue
deleted file mode 100644
index 85114fc..0000000
--- a/resources/js/pages/admin/manage/stocks/table/StockVerifyDialog.vue
+++ /dev/null
@@ -1,347 +0,0 @@
-
-
-
-
-
diff --git a/resources/js/pages/admin/manage/stocks/table/data-table-actions.vue b/resources/js/pages/admin/manage/stocks/table/data-table-actions.vue
deleted file mode 100644
index cdbde79..0000000
--- a/resources/js/pages/admin/manage/stocks/table/data-table-actions.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/resources/js/types/cutting.ts b/resources/js/types/cutting.ts
index caa15e1..922bc41 100644
--- a/resources/js/types/cutting.ts
+++ b/resources/js/types/cutting.ts
@@ -36,39 +36,12 @@ export type CuttingMaterialListItem = {
};
};
-export type CuttingResultPriceItem = {
- id: number;
- product_variant_id: number;
- price_type: string;
- price: number;
- price_formatted: string;
- cost_per_unit: number;
- cost_per_unit_formatted: string;
- type_label: string;
- product_variant?: {
- id: number;
- name: string;
- product?: {
- id: number;
- name: string;
- };
- };
-};
-
export type CuttingResultListItem = {
id: number;
cutting_result: number;
sample: number;
original_outside_sample: number;
- product_variant?: {
- id: number;
- name: string;
- images?: MediaItem[];
- product?: {
- id: number;
- name: string;
- };
- };
+ product_name?: string;
};
export type CuttingListItem = {
@@ -117,7 +90,6 @@ export type CuttingListItem = {
} | null;
materials: CuttingMaterialListItem[];
results: CuttingResultListItem[];
- result_prices?: CuttingResultPriceItem[];
};
export type CuttingMaterialCartItem = {
@@ -135,14 +107,10 @@ export type CuttingMaterialCartItem = {
};
export type CuttingResultCartItem = {
- product_variant_id: number;
product_name: string;
- variant_name: string;
- stock: number;
cutting_result: string | null;
sample: string | null;
original_outside_sample: string | null;
- images?: MediaItem[];
};
export type CuttingEditItem = {
@@ -176,18 +144,10 @@ export type CuttingEditItem = {
};
}>;
results: Array<{
- product_variant_id: number;
+ product_name?: string;
cutting_result: number;
sample: number;
original_outside_sample: number;
- product_variant?: {
- name: string;
- stock: number;
- images?: MediaItem[];
- product?: {
- name: string;
- };
- };
}>;
};
diff --git a/routes/web.php b/routes/web.php
index 5b2cda5..3a88067 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -24,7 +24,6 @@
use App\Http\Controllers\Admin\Manage\Restock\RestockController;
use App\Http\Controllers\Admin\Manage\Restock\RestockDraftItemController;
use App\Http\Controllers\Admin\Manage\Stock\RetailStockController;
-use App\Http\Controllers\Admin\Manage\Stock\StockController;
use App\Http\Controllers\Admin\Manage\StokOpnameController;
use App\Http\Controllers\Admin\Master\CategoryController;
use App\Http\Controllers\Admin\Master\CustomerController;
@@ -378,7 +377,7 @@
->name('destroy');
Route::post('{cutting}/status', [CuttingController::class, 'transitionStatus'])
- ->middleware('permission:'.Permission::CUTTINGS_COMPLETE->value.'|'.Permission::CUTTINGS_VERIFY->value.'|'.Permission::CUTTINGS_REJECT->value)
+ ->middleware('permission:'.Permission::CUTTINGS_COMPLETE->value)
->name('transition_status');
Route::post('draft-materials', [CuttingDraftItemController::class, 'storeMaterial'])
@@ -393,7 +392,7 @@
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
->name('draft_results.store');
- Route::delete('draft-results/{productVariant}', [CuttingDraftItemController::class, 'destroyResult'])
+ Route::delete('draft-results/{cuttingResult}', [CuttingDraftItemController::class, 'destroyResult'])
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
->name('draft_results.destroy');
@@ -410,16 +409,6 @@
->name('quick_create_product');
});
- Route::prefix('stocks')->name('stocks.')
- ->middleware('permission:'.Permission::STOCKS_VIEW->value)
- ->group(function () {
- Route::get('/', [StockController::class, 'index'])->name('index');
-
- Route::post('{cutting}/verify', [StockController::class, 'verify'])
- ->middleware('permission:'.Permission::CUTTINGS_VERIFY->value)
- ->name('verify');
- });
-
Route::prefix('retail-stock')->name('retail-stock.')
->middleware('permission:'.Permission::STOCKS_VIEW->value)
->group(function () {
@@ -472,14 +461,6 @@
Route::get('requests/{ownerVerificationRequest}', [OwnerVerificationController::class, 'show'])
->name('show');
- Route::post('cuttings/{cutting}/approve', [OwnerVerificationController::class, 'approveCutting'])
- ->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VERIFY->value)
- ->name('approve');
-
- Route::post('cuttings/{cutting}/reject', [OwnerVerificationController::class, 'rejectCutting'])
- ->middleware('permission:'.Permission::OWNER_VERIFICATIONS_REJECT->value)
- ->name('reject');
-
Route::post('requests/{ownerVerificationRequest}/approve', [OwnerVerificationController::class, 'approveRequest'])
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VERIFY->value)
->name('approve_request');
diff --git a/tests/Feature/Admin/Manage/CuttingTest.php b/tests/Feature/Admin/Manage/CuttingTest.php
index 03482f0..24603b4 100644
--- a/tests/Feature/Admin/Manage/CuttingTest.php
+++ b/tests/Feature/Admin/Manage/CuttingTest.php
@@ -8,7 +8,6 @@
use App\Models\CuttingMaterialCombination;
use App\Models\CuttingResult;
use App\Models\Product;
-use App\Models\ProductVariant;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\User;
@@ -51,17 +50,14 @@ function createCuttingWithMaterialsAndResults(?User $user = null): Cutting
$rawMaterial = RawMaterial::factory()->create();
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
- CuttingMaterial::factory()->create([
+ CuttingResult::factory()->create([
'cutting_id' => $cutting->id,
- 'raw_material_price_id' => $price->id,
+ 'product_name' => 'Test Product',
]);
CuttingResult::factory()->create([
'cutting_id' => $cutting->id,
- 'product_variant_id' => $variant->id,
+ 'product_name' => 'Second Product',
]);
return $cutting;
@@ -72,10 +68,17 @@ function setupDraftItems(User $user): array
$rawMaterial = RawMaterial::factory()->create();
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
+ return ['price' => $price];
+}
- return ['price' => $price, 'variant' => $variant];
+function makeResultData(): array
+{
+ return [
+ 'product_name' => 'Test Product',
+ 'cutting_result' => 10,
+ 'sample' => 8,
+ 'original_outside_sample' => 2,
+ ];
}
// ─── Index ────────────────────────────────────────────────
@@ -168,22 +171,13 @@ function setupDraftItems(User $user): array
])
->assertOk();
- // Create draft result
- $this->actingAs($user)
- ->postJson(route('admin.manage.cuttings.draft_results.store'), [
- 'product_variant_id' => $draft['variant']->id,
- 'cutting_result' => 10,
- 'sample' => 8,
- 'original_outside_sample' => 2,
- ])
- ->assertOk();
-
- // Store cutting
+ // Store cutting with results directly
$this->actingAs($user)
->post(route('admin.manage.cuttings.store'), [
'description' => 'Cutting test',
'sewing_cost' => 50000,
'other_cost' => 10000,
+ 'results' => [makeResultData()],
])
->assertRedirect(route('admin.manage.cuttings.index'));
@@ -228,20 +222,11 @@ function setupDraftItems(User $user): array
])
->assertOk();
- // Create draft result
- $this->actingAs($user)
- ->postJson(route('admin.manage.cuttings.draft_results.store'), [
- 'product_variant_id' => $draft['variant']->id,
- 'cutting_result' => 10,
- 'sample' => 8,
- 'original_outside_sample' => 2,
- ])
- ->assertOk();
-
- // Store cutting
+ // Store cutting with results directly
$this->actingAs($user)
->post(route('admin.manage.cuttings.store'), [
'description' => 'Cutting with result',
+ 'results' => [makeResultData()],
])
->assertRedirect(route('admin.manage.cuttings.index'));
@@ -268,31 +253,21 @@ function setupDraftItems(User $user): array
test('store fails without draft materials', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
- $draft = setupDraftItems($user);
-
- // Only create draft result, no material
- $this->actingAs($user)
- ->postJson(route('admin.manage.cuttings.draft_results.store'), [
- 'product_variant_id' => $draft['variant']->id,
- 'cutting_result' => 10,
- 'sample' => 8,
- 'original_outside_sample' => 2,
- ])
- ->assertOk();
-
+ // Send results but no draft material
$this->actingAs($user)
->post(route('admin.manage.cuttings.store'), [
'description' => 'Test',
+ 'results' => [makeResultData()],
])
->assertSessionHasErrors('materials');
});
- test('store fails without draft results', function () {
+ test('store fails without results', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$draft = setupDraftItems($user);
- // Only create draft material, no result
+ // Create draft material but don't send results
$this->actingAs($user)
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
'raw_material_price_id' => $draft['price']->id,
@@ -360,9 +335,6 @@ function setupDraftItems(User $user): array
$rawMaterial = RawMaterial::factory()->create();
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
$this->actingAs($user)
->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'Updated description',
@@ -373,7 +345,7 @@ function setupDraftItems(User $user): array
],
'results' => [
[
- 'product_variant_id' => $variant->id,
+ 'product_name' => 'Test Product',
'cutting_result' => 20,
'sample' => 15,
'original_outside_sample' => 5,
@@ -393,9 +365,6 @@ function setupDraftItems(User $user): array
$rawMaterial = RawMaterial::factory()->create();
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
$this->actingAs($user)
->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'With material result',
@@ -406,7 +375,7 @@ function setupDraftItems(User $user): array
],
'results' => [
[
- 'product_variant_id' => $variant->id,
+ 'product_name' => 'Test Product',
'cutting_result' => 20,
'sample' => 15,
'original_outside_sample' => 5,
@@ -425,7 +394,7 @@ function setupDraftItems(User $user): array
$this->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'Test',
'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]],
- 'results' => [['product_variant_id' => 1, 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
+ 'results' => [['product_name' => 'P', 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
])->assertRedirect(route('login'));
});
@@ -438,7 +407,7 @@ function setupDraftItems(User $user): array
->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'Test',
'materials' => [['raw_material_price_id' => 1, 'material_usage' => 1]],
- 'results' => [['product_variant_id' => 1, 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
+ 'results' => [['product_name' => 'P', 'cutting_result' => 1, 'sample' => 1, 'original_outside_sample' => 0]],
])
->assertForbidden();
});
@@ -530,27 +499,11 @@ function setupDraftItems(User $user): array
->assertForbidden();
});
- test('invalid status transition is rejected', function () {
- $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_VERIFY);
+ test('completed cutting cannot transition to another status', function () {
+ $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_UPDATE);
$cutting = createCuttingWithMaterialsAndResults($user);
- // IN_PROGRESS cannot go directly to VERIFIED
- $this->actingAs($user)
- ->post(route('admin.manage.cuttings.transition_status', $cutting), [
- 'status' => CuttingStatus::VERIFIED->value,
- ])
- ->assertSessionHasErrors('status');
- });
-
- test('completed cutting can be verified by owner', function () {
- $user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_COMPLETE, PermissionEnum::CUTTINGS_VERIFY);
- $user->assignRole('owner');
- $user->forgetCachedPermissions();
-
- $cutting = createCuttingWithMaterialsAndResults($user);
-
- // First complete
$this->actingAs($user)
->post(route('admin.manage.cuttings.transition_status', $cutting), [
'status' => CuttingStatus::COMPLETED->value,
@@ -558,30 +511,12 @@ function setupDraftItems(User $user): array
expect($cutting->fresh()->status)->toBe(CuttingStatus::COMPLETED);
- // Then verify with result_prices
- $result = $cutting->results->first();
-
+ // COMPLETED is final, cannot transition to another status
$this->actingAs($user)
->post(route('admin.manage.cuttings.transition_status', $cutting), [
- 'status' => CuttingStatus::VERIFIED->value,
- 'results' => [
- [
- 'product_variant_id' => $result->product_variant_id,
- 'sample' => $result->sample ?? 0,
- 'original_outside_sample' => $result->original_outside_sample ?? 0,
- ],
- ],
- 'result_prices' => [
- [
- 'product_variant_id' => $result->product_variant_id,
- 'prices' => [
- ['type' => 'retail', 'price' => 150000],
- ],
- ],
- ],
- ]);
-
- expect($cutting->fresh()->status)->toBe(CuttingStatus::VERIFIED);
+ 'status' => CuttingStatus::IN_PROGRESS->value,
+ ])
+ ->assertSessionHasErrors('status');
});
});
@@ -812,9 +747,6 @@ function setupDraftItems(User $user): array
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
// Create draft combination
$this->actingAs($user)
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
@@ -825,20 +757,11 @@ function setupDraftItems(User $user): array
])
->assertOk();
- // Create draft result
- $this->actingAs($user)
- ->postJson(route('admin.manage.cuttings.draft_results.store'), [
- 'product_variant_id' => $variant->id,
- 'cutting_result' => 10,
- 'sample' => 8,
- 'original_outside_sample' => 2,
- ])
- ->assertOk();
-
- // Store cutting
+ // Store cutting with results directly
$this->actingAs($user)
->post(route('admin.manage.cuttings.store'), [
'description' => 'Cutting with combination',
+ 'results' => [makeResultData()],
])
->assertRedirect(route('admin.manage.cuttings.index'));
@@ -862,9 +785,6 @@ function setupDraftItems(User $user): array
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
$this->actingAs($user)
->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'Updated with combination',
@@ -876,7 +796,7 @@ function setupDraftItems(User $user): array
],
'results' => [
[
- 'product_variant_id' => $variant->id,
+ 'product_name' => 'Test Product',
'cutting_result' => 10,
'sample' => 8,
'original_outside_sample' => 2,
@@ -902,9 +822,6 @@ function setupDraftItems(User $user): array
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create(['product_id' => $product->id]);
-
$this->actingAs($user)
->put(route('admin.manage.cuttings.update', $cutting), [
'description' => 'Combination with result',
@@ -916,7 +833,7 @@ function setupDraftItems(User $user): array
],
'results' => [
[
- 'product_variant_id' => $variant->id,
+ 'product_name' => 'Test Product',
'cutting_result' => 10,
'sample' => 8,
'original_outside_sample' => 2,
@@ -977,12 +894,10 @@ function setupDraftItems(User $user): array
expect($cutting->createdBy->id)->toBe($user->id);
});
- test('cutting status is editable for in_progress and rejected', function () {
+ test('cutting status is editable for in_progress', function () {
$inProgress = Cutting::factory()->create(['status' => CuttingStatus::IN_PROGRESS->value]);
- $rejected = Cutting::factory()->create(['status' => CuttingStatus::REJECTED->value]);
expect($inProgress->status->isEditable())->toBeTrue();
- expect($rejected->status->isEditable())->toBeTrue();
});
});
diff --git a/tests/Feature/Admin/Manage/StockTest.php b/tests/Feature/Admin/Manage/StockTest.php
deleted file mode 100644
index c038e85..0000000
--- a/tests/Feature/Admin/Manage/StockTest.php
+++ /dev/null
@@ -1,364 +0,0 @@
-seed(RolePermissionSeeder::class);
-});
-
-// ─── Helper ───────────────────────────────────────────────
-
-function createStockUserWithPermission(PermissionEnum ...$permissions): User
-{
- $user = User::factory()->create();
-
- $user->givePermissionTo(
- array_merge(
- [PermissionEnum::DASHBOARD_VIEW->value],
- array_map(fn (PermissionEnum $p) => $p->value, $permissions)
- )
- );
-
- $user->forgetCachedPermissions();
-
- return $user;
-}
-
-function createCompletedCuttingSetup(): array
-{
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 10,
- 'reject_stock' => 5,
- ]);
-
- $rawMaterial = RawMaterial::factory()->create();
- $rawMaterialPrice = RawMaterialPrice::factory()->create([
- 'raw_material_id' => $rawMaterial->id,
- ]);
-
- $cutting = Cutting::factory()->create([
- 'status' => CuttingStatus::COMPLETED,
- ]);
-
- CuttingMaterial::factory()->create([
- 'cutting_id' => $cutting->id,
- 'raw_material_price_id' => $rawMaterialPrice->id,
- ]);
-
- CuttingResult::factory()->create([
- 'cutting_id' => $cutting->id,
- 'product_variant_id' => $variant->id,
- 'cutting_result' => 7,
- 'sample' => 5, // Good stock result
- 'original_outside_sample' => 2, // Reject stock result
- ]);
-
- return [
- 'cutting' => $cutting,
- 'variant' => $variant,
- 'raw_material_price' => $rawMaterialPrice,
- ];
-}
-
-// ─── Tests ────────────────────────────────────────────────
-
-describe('Stock Warehouse Management', function () {
- test('authenticated user with permission can view stocks index', function () {
- $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
-
- $this->actingAs($user)
- ->get(route('admin.manage.stocks.index'))
- ->assertOk();
- });
-
- test('user with permission can submit verification for completed cutting', function () {
- $user = createStockUserWithPermission(
- PermissionEnum::STOCKS_VIEW,
- PermissionEnum::CUTTINGS_VERIFY
- );
-
- $setup = createCompletedCuttingSetup();
- $cutting = $setup['cutting'];
-
- $payload = [
- 'verification_note' => 'Verification notes',
- 'results' => [
- [
- 'product_variant_id' => $setup['variant']->id,
- 'good' => 6, // changed from 5 to 6
- 'reject' => 1, // changed from 2 to 1
- ],
- ],
- 'result_prices' => [
- [
- 'product_variant_id' => $setup['variant']->id,
- 'prices' => [
- [
- 'type' => 'harga_modal',
- 'price' => 50000,
- ],
- [
- 'type' => 'retail',
- 'price' => 75000,
- ],
- ],
- ],
- ],
- ];
-
- $this->actingAs($user)
- ->post(route('admin.manage.stocks.verify', $cutting), $payload)
- ->assertRedirect(route('admin.manage.stocks.index'));
-
- $cutting->refresh();
- $this->assertEquals(CuttingStatus::PENDING_VERIFICATION, $cutting->status);
-
- // Verify cutting results are updated
- $this->assertDatabaseHas('cutting_results', [
- 'cutting_id' => $cutting->id,
- 'product_variant_id' => $setup['variant']->id,
- 'sample' => 6,
- 'original_outside_sample' => 1,
- ]);
- });
-
- test('user with permission can approve verification and increase stock', function () {
- $user = createStockUserWithPermission(
- PermissionEnum::STOCKS_VIEW,
- PermissionEnum::OWNER_VERIFICATIONS_VERIFY
- );
-
- $setup = createCompletedCuttingSetup();
- $cutting = $setup['cutting'];
- $cutting->status = CuttingStatus::PENDING_VERIFICATION;
- $cutting->save();
-
- $this->actingAs($user)
- ->post(route('admin.manage.owner_verifications.approve', $cutting), [
- 'approval_note' => 'Approved by owner',
- ])
- ->assertRedirect();
-
- $cutting->refresh();
- $setup['variant']->refresh();
-
- $this->assertEquals(CuttingStatus::VERIFIED, $cutting->status);
-
- // Stock (good) should increment by 5 (10 + 5 = 15)
- $this->assertEquals(15, $setup['variant']->stock);
-
- // Reject stock should increment by 2 (5 + 2 = 7)
- $this->assertEquals(7, $setup['variant']->reject_stock);
- });
-
- test('user with permission can reject verification', function () {
- $user = createStockUserWithPermission(
- PermissionEnum::STOCKS_VIEW,
- PermissionEnum::OWNER_VERIFICATIONS_REJECT
- );
-
- $setup = createCompletedCuttingSetup();
- $cutting = $setup['cutting'];
- $cutting->status = CuttingStatus::PENDING_VERIFICATION;
- $cutting->save();
-
- $this->actingAs($user)
- ->post(route('admin.manage.owner_verifications.reject', $cutting), [
- 'reason' => 'Invalid results',
- ])
- ->assertRedirect();
-
- $cutting->refresh();
-
- // Status should revert to COMPLETED
- $this->assertEquals(CuttingStatus::COMPLETED, $cutting->status);
-
- $this->assertDatabaseHas('rejections', [
- 'rejectable_id' => $cutting->id,
- 'rejectable_type' => Cutting::class,
- 'reason' => 'Invalid results',
- ]);
- });
-});
-
-// ─── Retail Stock Transfer ──────────────────────────────────
-
-describe('Retail Stock Transfer', function () {
- test('user with permission can transfer stock from good to retail', function () {
- $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
-
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 10,
- 'retail_stock' => 5,
- ]);
-
- $this->actingAs($user)
- ->postJson(route('admin.manage.retail-stock.transfer'), [
- 'product_variant_id' => $variant->id,
- 'quantity' => 3,
- 'notes' => 'Transfer test',
- ])
- ->assertOk();
-
- $variant->refresh();
-
- $this->assertEquals(7, $variant->stock);
- $this->assertEquals(8, $variant->retail_stock);
-
- $this->assertDatabaseHas('retail_stock_histories', [
- 'product_variant_id' => $variant->id,
- 'user_id' => $user->id,
- 'quantity' => 3,
- 'stock_before' => 10,
- 'retail_stock_before' => 5,
- 'stock_after' => 7,
- 'retail_stock_after' => 8,
- 'notes' => 'Transfer test',
- ]);
- });
-
- test('retail stock transfer does not require owner verification', function () {
- $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
-
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 10,
- 'retail_stock' => 5,
- ]);
-
- $this->actingAs($user)
- ->postJson(route('admin.manage.retail-stock.transfer'), [
- 'product_variant_id' => $variant->id,
- 'quantity' => 3,
- ])
- ->assertOk();
-
- $this->assertDatabaseCount('owner_verification_requests', 0);
-
- $variant->refresh();
- $this->assertEquals(7, $variant->stock);
- $this->assertEquals(8, $variant->retail_stock);
- });
-
- test('retail stock transfer fails when quantity exceeds stock', function () {
- $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
-
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 5,
- 'retail_stock' => 0,
- ]);
-
- $this->actingAs($user)
- ->postJson(route('admin.manage.retail-stock.transfer'), [
- 'product_variant_id' => $variant->id,
- 'quantity' => 10,
- ])
- ->assertUnprocessable()
- ->assertJsonValidationErrors('quantity');
- });
-
- test('retail stock transfer fails when quantity is zero', function () {
- $user = createStockUserWithPermission(PermissionEnum::STOCKS_VIEW);
-
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 10,
- 'retail_stock' => 5,
- ]);
-
- $this->actingAs($user)
- ->postJson(route('admin.manage.retail-stock.transfer'), [
- 'product_variant_id' => $variant->id,
- 'quantity' => 0,
- ])
- ->assertUnprocessable()
- ->assertJsonValidationErrors('quantity');
- });
-
- test('user without permission cannot transfer stock', function () {
- $user = User::factory()->create();
-
- $product = Product::factory()->create();
- $variant = ProductVariant::factory()->create([
- 'product_id' => $product->id,
- 'stock' => 10,
- 'retail_stock' => 5,
- ]);
-
- $this->actingAs($user)
- ->postJson(route('admin.manage.retail-stock.transfer'), [
- 'product_variant_id' => $variant->id,
- 'quantity' => 3,
- ])
- ->assertForbidden();
- });
-
- test('owner can verify stock directly without pending verification', function () {
- $user = createStockUserWithPermission(
- PermissionEnum::STOCKS_VIEW,
- PermissionEnum::CUTTINGS_VERIFY,
- PermissionEnum::OWNER_VERIFICATIONS_VERIFY
- );
-
- $setup = createCompletedCuttingSetup();
- $cutting = $setup['cutting'];
-
- $payload = [
- 'verification_note' => 'Verified directly by owner',
- 'results' => [
- [
- 'product_variant_id' => $setup['variant']->id,
- 'good' => 6,
- 'reject' => 1,
- ],
- ],
- 'result_prices' => [
- [
- 'product_variant_id' => $setup['variant']->id,
- 'prices' => [
- [
- 'type' => 'harga_modal',
- 'price' => 50000,
- ],
- [
- 'type' => 'retail',
- 'price' => 75000,
- ],
- ],
- ],
- ],
- ];
-
- $this->actingAs($user)
- ->post(route('admin.manage.stocks.verify', $cutting), $payload)
- ->assertRedirect(route('admin.manage.stocks.index'));
-
- $cutting->refresh();
- $this->assertEquals(CuttingStatus::VERIFIED, $cutting->status);
-
- $setup['variant']->refresh();
- // Base stock was 10. Added good stock is 6.
- $this->assertEquals(16, $setup['variant']->stock);
- });
-});