diff --git a/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php b/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php index ec2e314..c334947 100644 --- a/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php +++ b/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Master\Product\ProductVariantRequest; +use App\Http\Requests\Admin\Master\Product\TransferStockRequest; use App\Models\Product; use App\Models\ProductVariant; use App\Services\Admin\Master\Product\ProductVariantService; @@ -43,4 +44,13 @@ public function destroy(Product $product, ProductVariant $variant): RedirectResp 'admin.master.products.index' ); } + + public function transferStock(TransferStockRequest $request, Product $product, ProductVariant $variant): RedirectResponse + { + return $this->handleAction( + fn () => $this->variantService->transferStock($variant, $request->validated()), + 'Transfer stok berhasil dilakukan.', + 'admin.master.products.index' + ); + } } diff --git a/app/Http/Requests/Admin/Master/Product/TransferStockRequest.php b/app/Http/Requests/Admin/Master/Product/TransferStockRequest.php new file mode 100644 index 0000000..caa235a --- /dev/null +++ b/app/Http/Requests/Admin/Master/Product/TransferStockRequest.php @@ -0,0 +1,29 @@ + ['required', 'integer', 'min:1'], + 'description' => ['nullable', 'string', 'max:500'], + ]; + } + + public function attributes(): array + { + return [ + 'quantity' => 'Jumlah Transfer', + 'description' => 'Keterangan', + ]; + } +} diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php index aa6427c..6fa54db 100644 --- a/app/Models/ProductVariant.php +++ b/app/Models/ProductVariant.php @@ -45,4 +45,10 @@ public function stokOpnameItems(): HasMany { return $this->hasMany(StokOpnameItem::class); } + + public function stockMutations(): HasMany + { + return $this->hasMany(StockMutation::class, 'stockable_id') + ->where('stockable_type', self::class); + } } diff --git a/app/Services/Admin/Master/Product/ProductVariantService.php b/app/Services/Admin/Master/Product/ProductVariantService.php index 5214258..4a74d8d 100644 --- a/app/Services/Admin/Master/Product/ProductVariantService.php +++ b/app/Services/Admin/Master/Product/ProductVariantService.php @@ -5,10 +5,12 @@ use App\Models\Product; use App\Models\ProductPrice; use App\Models\ProductVariant; +use App\Models\StockMutation; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; class ProductVariantService { @@ -113,4 +115,58 @@ public function getTemporaryUrl(ProductVariant $variant): ?string return $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null; } + + public function transferStock(ProductVariant $variant, array $data): ProductVariant + { + $quantity = (int) $data['quantity']; + + if ($variant->stock < $quantity) { + throw ValidationException::withMessages([ + 'quantity' => "Stok bagus tidak mencukupi. Stok tersedia: {$variant->stock}.", + ]); + } + + DB::transaction(function () use ($variant, $quantity, $data) { + $stockBefore = $variant->stock; + $retailBefore = $variant->retail_stock; + + $variant->update([ + 'stock' => $stockBefore - $quantity, + 'retail_stock' => $retailBefore + $quantity, + ]); + + StockMutation::create([ + 'user_id' => auth()->id(), + 'stockable_type' => ProductVariant::class, + 'stockable_id' => $variant->id, + 'type' => 'out', + 'quantity' => -$quantity, + 'stock_before' => $stockBefore, + 'stock_after' => $stockBefore - $quantity, + 'stock_quality' => 'good', + 'description' => $data['description'] ?? 'Transfer stok bagus ke stok ecer', + ]); + + StockMutation::create([ + 'user_id' => auth()->id(), + 'stockable_type' => ProductVariant::class, + 'stockable_id' => $variant->id, + 'type' => 'in', + 'quantity' => $quantity, + 'stock_before' => $retailBefore, + 'stock_after' => $retailBefore + $quantity, + 'stock_quality' => 'retail', + 'description' => $data['description'] ?? 'Transfer stok bagus ke stok ecer', + ]); + }); + + NotificationService::notify( + roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], + title: 'Transfer Stok', + body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.', + url: route('admin.master.products.index'), + ); + + return $variant->fresh(); + } } diff --git a/resources/js/pages/admin/master/product/variant/sub-row.tsx b/resources/js/pages/admin/master/product/variant/sub-row.tsx index 91b4d75..0a327fe 100644 --- a/resources/js/pages/admin/master/product/variant/sub-row.tsx +++ b/resources/js/pages/admin/master/product/variant/sub-row.tsx @@ -1,4 +1,4 @@ -import { Pencil, Trash2 } from 'lucide-react'; +import { ArrowRightLeft, Pencil, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { ImagePreviewModal } from '@/components/image-preview-modal'; import { Button } from '@/components/ui/button'; @@ -16,7 +16,8 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; -import type { Product, ProductVariant } from './columns'; +import type { Product, ProductVariant } from '../columns'; +import { TransferStockDialog } from './transfer-stock-dialog'; function formatCurrency(amount: number): string { return new Intl.NumberFormat('id-ID', { @@ -65,6 +66,10 @@ export function VariantSubRow({ onDeleteVariantClick: (product: Product, variant: ProductVariant) => void; }) { const variants = product.product_variants ?? []; + const [transferVariant, setTransferVariant] = useState<{ + product: Product; + variant: ProductVariant; + } | null>(null); return (
@@ -84,7 +89,7 @@ export function VariantSubRow({ Stok Ecer Harga - + Aksi @@ -151,6 +156,25 @@ export function VariantSubRow({
+ + + + + + Transfer Stok + +
); } diff --git a/resources/js/pages/admin/master/product/variant/transfer-stock-dialog.tsx b/resources/js/pages/admin/master/product/variant/transfer-stock-dialog.tsx new file mode 100644 index 0000000..d174ba2 --- /dev/null +++ b/resources/js/pages/admin/master/product/variant/transfer-stock-dialog.tsx @@ -0,0 +1,121 @@ +import { Form } from '@inertiajs/react'; +import InputError from '@/components/input-error'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { transferStock } from '@/routes/admin/master/products/variants'; +import type { Product, ProductVariant } from '../columns'; + +type TransferStockDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + product: Product; + variant: ProductVariant; +}; + +export function TransferStockDialog({ + open, + onOpenChange, + product, + variant, +}: TransferStockDialogProps) { + return ( + + +
onOpenChange(false)} + > + {({ errors, processing }) => ( + <> + + + Transfer Stok Bagus → Stok Ecer + + +
+
+ +

+ {product.name} +

+
+
+ +

+ {variant.name} +

+
+
+ +

+ {variant.stock} +

+
+
+ + + +
+
+ +