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