feat: implement product stock management with automated inventory tracking during purchases
This commit is contained in:
parent
f1cda67e23
commit
c48d2ecd18
@ -56,6 +56,8 @@ public function store(PurchaseRequest $request): RedirectResponse
|
||||
'unit_price' => $item['unit_price'],
|
||||
'total_price' => $item['quantity'] * $item['unit_price'],
|
||||
]);
|
||||
|
||||
Product::find($item['product_id'])->increment('stock', $item['quantity']);
|
||||
}
|
||||
|
||||
PurchaseItem::where('user_id', auth()->id())
|
||||
@ -89,6 +91,10 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
||||
'total' => $total,
|
||||
]);
|
||||
|
||||
foreach ($purchase->items as $item) {
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
|
||||
$purchase->items()->delete();
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
@ -99,6 +105,8 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
||||
'unit_price' => $item['unit_price'],
|
||||
'total_price' => $item['quantity'] * $item['unit_price'],
|
||||
]);
|
||||
|
||||
Product::find($item['product_id'])->increment('stock', $item['quantity']);
|
||||
}
|
||||
});
|
||||
|
||||
@ -107,7 +115,12 @@ public function update(PurchaseRequest $request, Purchase $purchase): RedirectRe
|
||||
|
||||
public function destroy(Purchase $purchase): RedirectResponse
|
||||
{
|
||||
$purchase->delete();
|
||||
DB::transaction(function () use ($purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
$purchase->delete();
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Data berhasil dihapus');
|
||||
}
|
||||
@ -116,7 +129,15 @@ public function bulkDestroy(Request $request): RedirectResponse
|
||||
{
|
||||
$ids = $request->input('ids');
|
||||
|
||||
Purchase::whereIn('id', $ids)->delete();
|
||||
DB::transaction(function () use ($ids) {
|
||||
$purchases = Purchase::with('items')->whereIn('id', $ids)->get();
|
||||
foreach ($purchases as $purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
$item->product->decrement('stock', $item->quantity);
|
||||
}
|
||||
$purchase->delete();
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->back()->with('success', 'Data terpilih berhasil dihapus');
|
||||
}
|
||||
|
||||
@ -87,6 +87,7 @@ public function update(ProductRequest $request, Product $product): RedirectRespo
|
||||
$product->update([
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'],
|
||||
'stock' => $validated['stock'] ?? 0,
|
||||
]);
|
||||
|
||||
$product->categories()->sync($validated['category_ids'] ?? []);
|
||||
|
||||
@ -40,6 +40,7 @@ public function rules(): array
|
||||
'images.*' => ['image', 'max:5120'],
|
||||
'delete_image_ids' => ['nullable', 'array'],
|
||||
'delete_image_ids.*' => ['integer'],
|
||||
'stock' => ['required', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'stock' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ public function up(): void
|
||||
$table->string('name', 100);
|
||||
$table->string('slug', 100);
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('stock')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
|
||||
@ -41,6 +41,8 @@ public function run(): void
|
||||
'total_price' => $totalPrice,
|
||||
]);
|
||||
|
||||
$product->increment('stock', $quantity);
|
||||
|
||||
$total += $totalPrice;
|
||||
}
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
|
||||
thumbnail: File | null;
|
||||
images: File[];
|
||||
delete_image_ids: number[];
|
||||
stock: string;
|
||||
_method: string;
|
||||
}>({
|
||||
name: product.name || '',
|
||||
@ -82,6 +83,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
|
||||
thumbnail: null,
|
||||
images: [],
|
||||
delete_image_ids: [],
|
||||
stock: product.stock?.toString() || '0',
|
||||
_method: 'PATCH',
|
||||
});
|
||||
|
||||
@ -179,19 +181,36 @@ export default function ProductEdit({ product, categories }: { product: Product,
|
||||
<CardTitle>Informasi Produk</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Field>
|
||||
<Label htmlFor="name" required>Nama</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: Gamis Wanita'
|
||||
maxLength={100}
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
|
||||
</Field>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Field>
|
||||
<Label htmlFor="name" required>Nama</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={e => setData('name', e.target.value)}
|
||||
autoComplete='off'
|
||||
placeholder='Contoh: Gamis Wanita'
|
||||
maxLength={100}
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="stock" required>Stok</Label>
|
||||
<NumericFormat
|
||||
id="stock"
|
||||
customInput={Input}
|
||||
thousandSeparator="."
|
||||
decimalSeparator=","
|
||||
value={data.stock}
|
||||
onValueChange={(values) => setData('stock', values.value)}
|
||||
placeholder="0"
|
||||
autoComplete='off'
|
||||
/>
|
||||
{errors.stock && <p className="text-xs text-red-500">{errors.stock}</p>}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="category_ids" required>Kategori</Label>
|
||||
|
||||
@ -105,6 +105,24 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
|
||||
},
|
||||
meta: { title: "Harga" },
|
||||
},
|
||||
{
|
||||
accessorKey: "stock",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Stok" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const product = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={product.stock <= 10 ? "destructive" : "outline"} className='font-mono'>
|
||||
{product.stock}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: { title: "Stok" },
|
||||
},
|
||||
{
|
||||
accessorKey: "is_active",
|
||||
header: "Status",
|
||||
|
||||
@ -25,6 +25,7 @@ export interface Product {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
stock: number;
|
||||
prices?: ProductPrice[];
|
||||
categories?: Category[];
|
||||
thumbnail_url?: string | null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user