feat: implement stock validation on purchase removal and add global toast error handling for flash messages

This commit is contained in:
Yoga Pangestu 2026-05-02 17:57:32 +07:00
parent 2d5d0b8eb1
commit 23cc07596c
17 changed files with 159 additions and 25 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

@ -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({});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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({});