diff --git a/app/Http/Controllers/Admin/Manage/OrderController.php b/app/Http/Controllers/Admin/Manage/OrderController.php index 7867e3b..b30d6ca 100644 --- a/app/Http/Controllers/Admin/Manage/OrderController.php +++ b/app/Http/Controllers/Admin/Manage/OrderController.php @@ -79,7 +79,11 @@ public function store(OrderRequest $request): RedirectResponse 'price_type' => $item['price_type'], ]); - Product::find($item['product_id'])->decrement('stock', $item['qty']); + $product = Product::find($item['product_id']); + if ($product->stock < $item['qty']) { + throw new \InvalidArgumentException("Stok produk {$product->name} tidak mencukupi. Sisa stok: {$product->stock}"); + } + $product->decrement('stock', $item['qty']); } OrderItem::where('user_id', auth()->id()) @@ -88,6 +92,8 @@ public function store(OrderRequest $request): RedirectResponse }); return redirect()->route('order.index')->with('success', 'Pesanan berhasil disimpan'); + } catch (\InvalidArgumentException $e) { + return redirect()->back()->withInput()->with('error', $e->getMessage()); } catch (\Throwable $e) { LogHelper::logException($e, 'Failed to store order', [ 'customer_name' => $validated['customer_name'], @@ -166,11 +172,17 @@ public function update(OrderRequest $request, Order $order): RedirectResponse 'price_type' => $item['price_type'], ]); - Product::find($item['product_id'])->decrement('stock', $item['qty']); + $product = Product::find($item['product_id']); + if ($product->stock < $item['qty']) { + throw new \InvalidArgumentException("Stok produk {$product->name} tidak mencukupi. Sisa stok: {$product->stock}"); + } + $product->decrement('stock', $item['qty']); } }); return redirect()->route('order.index')->with('success', 'Pesanan berhasil diperbarui'); + } catch (\InvalidArgumentException $e) { + return redirect()->back()->withInput()->with('error', $e->getMessage()); } catch (\Throwable $e) { LogHelper::logException($e, 'Failed to update order', [ 'order_id' => $order->id, diff --git a/app/Http/Controllers/Admin/Manage/PurchaseController.php b/app/Http/Controllers/Admin/Manage/PurchaseController.php index b3aafc1..9647a4d 100644 --- a/app/Http/Controllers/Admin/Manage/PurchaseController.php +++ b/app/Http/Controllers/Admin/Manage/PurchaseController.php @@ -106,6 +106,9 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe ]); foreach ($purchase->items as $item) { + if ($item->product->stock < $item->quantity) { + throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dibatalkan. Sisa stok: {$item->product->stock}"); + } $item->product->decrement('stock', $item->quantity); } @@ -125,6 +128,8 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe }); return redirect()->route('purchase.index')->with('success', 'Data berhasil diperbarui'); + } catch (\InvalidArgumentException $e) { + return redirect()->back()->withInput()->with('error', $e->getMessage()); } catch (\Throwable $e) { LogHelper::logException($e, 'Failed to update purchase', [ 'purchase_id' => $purchase->id, @@ -143,12 +148,17 @@ public function destroy(Purchase $purchase): RedirectResponse try { DB::transaction(function () use ($purchase) { foreach ($purchase->items as $item) { + if ($item->product->stock < $item->quantity) { + throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dihapus. Sisa stok: {$item->product->stock}"); + } $item->product->decrement('stock', $item->quantity); } $purchase->delete(); }); return redirect()->back()->with('success', 'Data berhasil dihapus'); + } catch (\InvalidArgumentException $e) { + return redirect()->back()->with('error', $e->getMessage()); } catch (\Throwable $e) { LogHelper::logException($e, 'Failed to delete purchase', [ 'purchase_id' => $purchase->id, @@ -164,9 +174,12 @@ public function bulkDestroy(Request $request): RedirectResponse try { DB::transaction(function () use ($ids) { - $purchases = Purchase::with('items')->whereIn('id', $ids)->get(); + $purchases = Purchase::with('items.product')->whereIn('id', $ids)->get(); foreach ($purchases as $purchase) { foreach ($purchase->items as $item) { + if ($item->product->stock < $item->quantity) { + throw new \InvalidArgumentException("Stok produk {$item->product->name} tidak mencukupi untuk dihapus. Sisa stok: {$item->product->stock}"); + } $item->product->decrement('stock', $item->quantity); } $purchase->delete(); @@ -174,6 +187,8 @@ public function bulkDestroy(Request $request): RedirectResponse }); return redirect()->back()->with('success', 'Data terpilih berhasil dihapus'); + } catch (\InvalidArgumentException $e) { + return redirect()->back()->with('error', $e->getMessage()); } catch (\Throwable $e) { LogHelper::logException($e, 'Failed to bulk delete purchases', [ 'ids' => $ids, diff --git a/database/migrations/2026_04_16_082802_create_products_table.php b/database/migrations/2026_04_16_082802_create_products_table.php index 5c1b7b0..cbf2ea0 100644 --- a/database/migrations/2026_04_16_082802_create_products_table.php +++ b/database/migrations/2026_04_16_082802_create_products_table.php @@ -16,7 +16,7 @@ public function up(): void $table->string('name', 100); $table->string('slug', 100); $table->text('description')->nullable(); - $table->integer('stock')->default(0); + $table->unsignedInteger('stock')->default(0); $table->boolean('is_active')->default(true); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); diff --git a/resources/js/pages/admin/manage/order/create.tsx b/resources/js/pages/admin/manage/order/create.tsx index 340c1f0..3c458a3 100644 --- a/resources/js/pages/admin/manage/order/create.tsx +++ b/resources/js/pages/admin/manage/order/create.tsx @@ -180,7 +180,12 @@ return; post(orderRoutes.store().url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/manage/order/edit.tsx b/resources/js/pages/admin/manage/order/edit.tsx index 96a59e3..26c2a9b 100644 --- a/resources/js/pages/admin/manage/order/edit.tsx +++ b/resources/js/pages/admin/manage/order/edit.tsx @@ -195,7 +195,12 @@ return; patch(orderRoutes.update(order.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/manage/purchase/create.tsx b/resources/js/pages/admin/manage/purchase/create.tsx index 625eaf3..f33ffa0 100644 --- a/resources/js/pages/admin/manage/purchase/create.tsx +++ b/resources/js/pages/admin/manage/purchase/create.tsx @@ -165,7 +165,12 @@ return; post(purchaseRoutes.store().url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/manage/purchase/edit.tsx b/resources/js/pages/admin/manage/purchase/edit.tsx index d89ea3b..92fe580 100644 --- a/resources/js/pages/admin/manage/purchase/edit.tsx +++ b/resources/js/pages/admin/manage/purchase/edit.tsx @@ -170,7 +170,12 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas patch(purchaseRoutes.update(purchase.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/manage/purchase/hooks/use-purchase-index.ts b/resources/js/pages/admin/manage/purchase/hooks/use-purchase-index.ts index 1e67fa9..80763d3 100644 --- a/resources/js/pages/admin/manage/purchase/hooks/use-purchase-index.ts +++ b/resources/js/pages/admin/manage/purchase/hooks/use-purchase-index.ts @@ -20,7 +20,12 @@ export function usePurchaseIndex() { if (purchaseToDelete) { router.delete(purchaseRoutes.destroy(purchaseToDelete.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsDeleteDialogOpen(false); setPurchaseToDelete(null); setRowSelection({}); @@ -35,7 +40,12 @@ export function usePurchaseIndex() { _method: 'DELETE' }, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsBulkDeleteDialogOpen(false); setRowsToDelete([]); setRowSelection({}); diff --git a/resources/js/pages/admin/master/product/create.tsx b/resources/js/pages/admin/master/product/create.tsx index a071e6a..c350916 100644 --- a/resources/js/pages/admin/master/product/create.tsx +++ b/resources/js/pages/admin/master/product/create.tsx @@ -113,7 +113,12 @@ export default function ProductCreate({ categories }: { categories: Category[] } post(productRoutes.store().url, { forceFormData: true, onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/master/product/edit.tsx b/resources/js/pages/admin/master/product/edit.tsx index e9caff2..93e957e 100644 --- a/resources/js/pages/admin/master/product/edit.tsx +++ b/resources/js/pages/admin/master/product/edit.tsx @@ -165,7 +165,12 @@ export default function ProductEdit({ product, categories }: { product: Product, post(productRoutes.update(product.id).url, { forceFormData: true, onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/master/product/hooks/use-product-index.ts b/resources/js/pages/admin/master/product/hooks/use-product-index.ts index 7a8870a..df77e1f 100644 --- a/resources/js/pages/admin/master/product/hooks/use-product-index.ts +++ b/resources/js/pages/admin/master/product/hooks/use-product-index.ts @@ -21,7 +21,12 @@ export function useProductIndex() { if (productToDelete) { router.delete(productRoutes.destroy(productToDelete.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsDeleteDialogOpen(false); setProductToDelete(null); setRowSelection({}); @@ -36,7 +41,12 @@ export function useProductIndex() { _method: 'DELETE' }, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsBulkDeleteDialogOpen(false); setRowsToDelete([]); setRowSelection({}); @@ -48,7 +58,14 @@ export function useProductIndex() { router.patch(productRoutes.toggleStatus(id).url, {}, { preserveState: true, preserveScroll: true, - onSuccess: (response: any) => toast.success(response.props.flash.success), + onSuccess: (response: any) => { + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } + }, }); }; diff --git a/resources/js/pages/admin/master/user/create.tsx b/resources/js/pages/admin/master/user/create.tsx index 1abdf67..2fd34d3 100644 --- a/resources/js/pages/admin/master/user/create.tsx +++ b/resources/js/pages/admin/master/user/create.tsx @@ -50,7 +50,12 @@ export default function UserCreate({ roles }: { roles: any[] }) { e.preventDefault(); post(userRoutes.store().url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/master/user/edit.tsx b/resources/js/pages/admin/master/user/edit.tsx index c1948d0..db57385 100644 --- a/resources/js/pages/admin/master/user/edit.tsx +++ b/resources/js/pages/admin/master/user/edit.tsx @@ -51,7 +51,12 @@ export default function UserEdit({ user, roles }: { user: any, roles: any[] }) { e.preventDefault(); post(userRoutes.update(user.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/master/user/hooks/use-user-index.ts b/resources/js/pages/admin/master/user/hooks/use-user-index.ts index 46abec8..b04ca09 100644 --- a/resources/js/pages/admin/master/user/hooks/use-user-index.ts +++ b/resources/js/pages/admin/master/user/hooks/use-user-index.ts @@ -22,7 +22,12 @@ export function useUserIndex() { if (userToReset) { router.patch(userRoutes.resetPassword(userToReset.id).url, {}, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsResetPasswordOpen(false); setUserToReset(null); }, @@ -39,7 +44,12 @@ export function useUserIndex() { if (userToDelete) { router.delete(userRoutes.destroy(userToDelete.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsDeleteDialogOpen(false); setUserToDelete(null); setRowSelection({}); @@ -54,7 +64,12 @@ export function useUserIndex() { _method: 'DELETE' }, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsBulkDeleteDialogOpen(false); setRowsToDelete([]); setRowSelection({}); @@ -67,7 +82,12 @@ export function useUserIndex() { preserveState: true, preserveScroll: true, onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/system/role/create.tsx b/resources/js/pages/admin/system/role/create.tsx index e06d458..5c91d4a 100644 --- a/resources/js/pages/admin/system/role/create.tsx +++ b/resources/js/pages/admin/system/role/create.tsx @@ -30,7 +30,12 @@ export default function RoleCreate({ e.preventDefault(); post(roleRoutes.store().url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/system/role/edit.tsx b/resources/js/pages/admin/system/role/edit.tsx index 4f51445..a0f8e11 100644 --- a/resources/js/pages/admin/system/role/edit.tsx +++ b/resources/js/pages/admin/system/role/edit.tsx @@ -32,7 +32,12 @@ export default function RoleEdit({ e.preventDefault(); put(roleRoutes.update(role.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } }, }); }; diff --git a/resources/js/pages/admin/system/role/hooks/use-role-index.ts b/resources/js/pages/admin/system/role/hooks/use-role-index.ts index 44fcd66..ccce0a5 100644 --- a/resources/js/pages/admin/system/role/hooks/use-role-index.ts +++ b/resources/js/pages/admin/system/role/hooks/use-role-index.ts @@ -20,7 +20,12 @@ export function useRoleIndex() { if (roleToDelete) { router.delete(roleRoutes.destroy(roleToDelete.id).url, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsDeleteDialogOpen(false); setRoleToDelete(null); setRowSelection({}); @@ -35,7 +40,12 @@ export function useRoleIndex() { _method: 'DELETE' }, { onSuccess: (response: any) => { - toast.success(response.props.flash.success); + const flash = response.props.flash; + if (flash?.error) { + toast.error(flash.error); + } else if (flash?.success) { + toast.success(flash.success); + } setIsBulkDeleteDialogOpen(false); setRowsToDelete([]); setRowSelection({});