diff --git a/app/Enums/CuttingStatus.php b/app/Enums/CuttingStatus.php index 9d56305..af9c7c9 100644 --- a/app/Enums/CuttingStatus.php +++ b/app/Enums/CuttingStatus.php @@ -11,6 +11,7 @@ enum CuttingStatus: string case IN_PROGRESS = 'in_progress'; case COMPLETED = 'completed'; + case PENDING_VERIFICATION = 'pending_verification'; case VERIFIED = 'verified'; case REJECTED = 'rejected'; @@ -19,6 +20,7 @@ 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', }; @@ -33,7 +35,8 @@ public function canTransitionTo(self $status): bool { return match ($this) { self::IN_PROGRESS => $status === self::COMPLETED, - self::COMPLETED => in_array($status, [self::VERIFIED, self::REJECTED, self::IN_PROGRESS], true), + self::COMPLETED => in_array($status, [self::PENDING_VERIFICATION, self::REJECTED, self::IN_PROGRESS], true), + self::PENDING_VERIFICATION => in_array($status, [self::VERIFIED, self::REJECTED, self::COMPLETED], true), self::REJECTED => $status === self::IN_PROGRESS, default => false, }; @@ -43,6 +46,7 @@ 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, @@ -66,6 +70,7 @@ public function availableActions(): array ], ], self::COMPLETED => [], + self::PENDING_VERIFICATION => [], self::REJECTED => [ [ 'status' => self::IN_PROGRESS->value, diff --git a/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php b/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php new file mode 100644 index 0000000..046bb15 --- /dev/null +++ b/app/Http/Controllers/Admin/Manage/OwnerVerificationController.php @@ -0,0 +1,64 @@ +user(); + + return Inertia::render('admin/manage/owner-verifications/Index', [ + 'pendingCuttings' => $this->stockService->getPendingApprovalCuttings($user), + ]); + } + + public function approve(Request $request, Cutting $cutting): RedirectResponse + { + $request->validate([ + 'approval_note' => ['nullable', 'string', 'max:500'], + ]); + + $this->stockService->approveVerification( + $cutting, + $request->user(), + $request->input('approval_note'), + ); + + $this->flashSuccess('Verifikasi berhasil disetujui. Stok produk telah ditambahkan ke toko.'); + + return redirect()->route('admin.manage.owner-verifications.index'); + } + + public function reject(Request $request, Cutting $cutting): RedirectResponse + { + $request->validate([ + 'reason' => ['required', 'string', 'max:500'], + ]); + + $this->stockService->rejectVerification( + $cutting, + $request->user(), + $request->input('reason'), + ); + + $this->flashSuccess('Verifikasi berhasil ditolak.'); + + return redirect()->route('admin.manage.owner-verifications.index'); + } +} diff --git a/app/Http/Controllers/Admin/Manage/StockController.php b/app/Http/Controllers/Admin/Manage/StockController.php index 5ee5fdb..db837ba 100644 --- a/app/Http/Controllers/Admin/Manage/StockController.php +++ b/app/Http/Controllers/Admin/Manage/StockController.php @@ -31,7 +31,7 @@ public function index(Request $request): Response public function verify(StockVerifyRequest $request, Cutting $cutting): RedirectResponse { - $this->stockService->verify( + $this->stockService->submitVerification( $cutting, $request->user(), $request->validated('verification_note'), @@ -39,7 +39,7 @@ public function verify(StockVerifyRequest $request, Cutting $cutting): RedirectR $request->validated('result_prices'), ); - $this->flashSuccess('Cutting berhasil diverifikasi. Stok produk telah ditambahkan ke toko.'); + $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 8b3a57e..7b11aae 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -73,6 +73,7 @@ public function share(Request $request): array 'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request), 'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request), 'pendingCuttings' => fn () => $this->pendingCuttings($request), + 'pendingOwnerVerifications' => fn () => $this->pendingOwnerVerifications($request), ]; } @@ -114,4 +115,17 @@ private function pendingCuttings(Request $request): int ->where('status', CuttingStatus::COMPLETED) ->count(); } + + private function pendingOwnerVerifications(Request $request): int + { + $user = $request->user(); + + if ($user === null || ! $user->can('cuttings.verify')) { + return 0; + } + + return Cutting::query() + ->where('status', CuttingStatus::PENDING_VERIFICATION) + ->count(); + } } diff --git a/app/Services/Manage/StockService.php b/app/Services/Manage/StockService.php index 8d1ce81..044a2e5 100644 --- a/app/Services/Manage/StockService.php +++ b/app/Services/Manage/StockService.php @@ -7,7 +7,6 @@ use App\Models\CuttingResultPrice; use App\Models\ProductPrice; use App\Models\ProductVariant; -use App\Models\RawMaterialPrice; use App\Models\User; use App\Services\System\PushNotificationService; use Illuminate\Support\Collection; @@ -44,11 +43,11 @@ public function getPendingVerificationCuttings(User $user): Collection } /** - * Get all verified cuttings (stock history). + * Get all cuttings pending owner approval. * * @return Collection */ - public function getVerifiedCuttings(): Collection + public function getPendingApprovalCuttings(User $user): Collection { return Cutting::query() ->with([ @@ -57,10 +56,10 @@ public function getVerifiedCuttings(): Collection 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', + 'resultPrices.productVariant:id,product_id,name', ]) - ->where('status', CuttingStatus::VERIFIED) + ->where('status', CuttingStatus::PENDING_VERIFICATION) ->latest() - ->limit(50) ->get() ->each(function (Cutting $cutting): void { $this->appendCostPreview($cutting); @@ -68,12 +67,12 @@ public function getVerifiedCuttings(): Collection } /** - * Verify a completed cutting - adds stock to store. + * Submit verification for a completed cutting - saves data but does NOT add stock yet. * * @param list|null $results * @param list}>|null $resultPrices */ - public function verify( + public function submitVerification( Cutting $cutting, User $user, ?string $verificationNote = null, @@ -101,8 +100,6 @@ public function verify( $cutting->load('results'); } - $this->applyProductStockOnVerify($cutting); - $this->storeResultPrices($cutting, $resultPrices ?? []); if ($verificationNote !== null && trim($verificationNote) !== '') { @@ -112,6 +109,47 @@ public function verify( ]); } + $cutting->status = CuttingStatus::PENDING_VERIFICATION; + $cutting->save(); + }); + + $description = $cutting->description ?? '-'; + + $this->pushNotificationService->sendToRoles( + '📦 Verifikasi Stok Menunggu Persetujuan', + "Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.", + ['owner', 'developer'], + '/admin/manage/owner-verifications', + ); + } + + /** + * Owner approves verification - adds stock to store. + */ + 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.', + ]); + } + + 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(); }); @@ -120,42 +158,42 @@ public function verify( $this->pushNotificationService->sendToRoles( '📦 Stok Cutting Diverifikasi', - "Cutting dengan deskripsi '{$description}' telah diverifikasi dan stok produk telah ditambahkan ke toko.", + "Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.", ['owner', 'developer'], - '/admin/manage/stocks', + '/admin/manage/owner-verifications', ); } /** - * Reject a completed cutting - sends back to in_progress. + * Owner rejects verification - sends back to completed. */ - public function reject(Cutting $cutting, User $user, string $reason): void - { - if ($cutting->status !== CuttingStatus::COMPLETED) { + public function rejectVerification( + Cutting $cutting, + User $user, + string $reason, + ): void { + if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) { throw ValidationException::withMessages([ - 'status' => 'Hanya cutting yang sudah selesai yang dapat ditolak.', + 'status' => 'Hanya cutting yang menunggu verifikasi yang dapat ditolak.', ]); } DB::transaction(function () use ($cutting, $user, $reason): void { - $cutting->load(['materials.rawMaterialPrice', 'results']); - $this->deductRemainingMaterialStock($cutting); - $cutting->rejection()->create([ 'reason' => trim($reason), 'rejected_by_id' => $user->id, ]); - $cutting->status = CuttingStatus::IN_PROGRESS; + $cutting->status = CuttingStatus::COMPLETED; $cutting->save(); }); $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( - '📦 Verifikasi Cutting Ditolak', - "Cutting dengan deskripsi '{$description}' ditolak dari verifikasi stok dengan alasan: '{$reason}'.", + '📦 Verifikasi Cutting Ditolak Owner', + "Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.", ['owner', 'developer'], - '/admin/manage/stocks', + '/admin/manage/owner-verifications', ); } @@ -176,17 +214,6 @@ private function applyProductStockOnVerify(Cutting $cutting): void } } - private function deductRemainingMaterialStock(Cutting $cutting): void - { - foreach ($cutting->materials as $material) { - if ((float) $material->remaining_material > 0) { - RawMaterialPrice::query() - ->whereKey($material->raw_material_price_id) - ->decrement('stock', $material->remaining_material); - } - } - } - /** * @param list}> $resultPrices */ @@ -224,22 +251,33 @@ private function storeResultPrices(Cutting $cutting, array $resultPrices): void 'cost_per_unit' => $costPerUnit, ], ); - - // Store/update in product_prices - ProductPrice::query()->updateOrCreate( - [ - 'variant_id' => $resultData['product_variant_id'], - 'type' => $priceData['type'], - ], - [ - 'price' => (int) $priceData['price'], - ], - ); } } } } + /** + * Update product_prices from cutting result prices (called on owner approval). + */ + 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 { $cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result')); diff --git a/database/migrations/2026_06_21_141248_add_pending_verification_status_to_cuttings_table.php b/database/migrations/2026_06_21_141248_add_pending_verification_status_to_cuttings_table.php new file mode 100644 index 0000000..5d01d71 --- /dev/null +++ b/database/migrations/2026_06_21_141248_add_pending_verification_status_to_cuttings_table.php @@ -0,0 +1,23 @@ + import { Link, usePage } from '@inertiajs/vue3'; -import { Banknote, CalendarDays, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue'; +import { Banknote, CalendarDays, CheckCircle, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue'; import { Sidebar, SidebarContent, @@ -24,7 +24,7 @@ interface MenuItem { href: string; icon: any; permission?: string; - badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings'; + badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings' | 'pendingOwnerVerifications'; } interface MenuGroup { @@ -55,6 +55,7 @@ const menuGroups: MenuGroup[] = [ { title: 'Belanja', href: '/admin/manage/purchases', icon: ShoppingBag, permission: 'purchases.view' }, { title: 'Cutting', href: '/admin/manage/cuttings', icon: Scissors, permission: 'cuttings.view' }, { title: 'Stok', href: '/admin/manage/stocks', icon: Warehouse, permission: 'cuttings.view', badgeKey: 'pendingCuttings' }, + { title: 'Verifikasi Owner', href: '/admin/manage/owner-verifications', icon: CheckCircle, permission: 'cuttings.verify', badgeKey: 'pendingOwnerVerifications' }, { title: 'Pesanan', href: '/admin/manage/orders', icon: ShoppingCart, permission: 'orders.view' }, ], }, diff --git a/resources/js/pages/admin/manage/owner-verifications/Index.vue b/resources/js/pages/admin/manage/owner-verifications/Index.vue new file mode 100644 index 0000000..abad0bf --- /dev/null +++ b/resources/js/pages/admin/manage/owner-verifications/Index.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/js/pages/admin/manage/owner-verifications/table/OwnerVerificationPendingSection.vue b/resources/js/pages/admin/manage/owner-verifications/table/OwnerVerificationPendingSection.vue new file mode 100644 index 0000000..8021021 --- /dev/null +++ b/resources/js/pages/admin/manage/owner-verifications/table/OwnerVerificationPendingSection.vue @@ -0,0 +1,91 @@ + + + diff --git a/resources/js/pages/admin/manage/owner-verifications/table/data-table-actions.vue b/resources/js/pages/admin/manage/owner-verifications/table/data-table-actions.vue new file mode 100644 index 0000000..6f501c3 --- /dev/null +++ b/resources/js/pages/admin/manage/owner-verifications/table/data-table-actions.vue @@ -0,0 +1,217 @@ + + +