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); -});