feat(inventory): 支援庫存新增不使用批號模式與自動累加邏輯
This commit is contained in:
@@ -163,7 +163,7 @@ class InventoryController extends Controller
|
||||
'items.*.productId' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
'items.*.unit_cost' => 'nullable|numeric|min:0', // 新增成本驗證
|
||||
'items.*.batchMode' => 'required|in:existing,new',
|
||||
'items.*.batchMode' => 'required|in:existing,new,none',
|
||||
'items.*.inventoryId' => 'required_if:items.*.batchMode,existing|nullable|exists:inventories,id',
|
||||
'items.*.originCountry' => 'required_if:items.*.batchMode,new|nullable|string|max:2',
|
||||
'items.*.expiryDate' => 'nullable|date',
|
||||
@@ -188,6 +188,26 @@ class InventoryController extends Controller
|
||||
if (isset($item['unit_cost'])) {
|
||||
$inventory->unit_cost = $item['unit_cost'];
|
||||
}
|
||||
} elseif ($item['batchMode'] === 'none') {
|
||||
// 模式 C:不使用批號 (自動累加至 NO-BATCH)
|
||||
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => 'NO-BATCH'
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'unit_cost' => $item['unit_cost'] ?? 0,
|
||||
'total_value' => 0,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => null,
|
||||
'origin_country' => 'TW',
|
||||
]
|
||||
);
|
||||
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
|
||||
@@ -548,7 +548,7 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
<TableCell>
|
||||
<div className="space-y-2">
|
||||
<SearchableSelect
|
||||
value={item.batchMode === 'new' ? 'new_batch' : (item.inventoryId || "")}
|
||||
value={item.batchMode === 'none' ? 'no_batch' : (item.batchMode === 'new' ? 'new_batch' : (item.inventoryId || ""))}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'new_batch') {
|
||||
handleUpdateItem(item.tempId, {
|
||||
@@ -557,6 +557,15 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
originCountry: 'TW',
|
||||
expiryDate: undefined
|
||||
});
|
||||
} else if (value === 'no_batch') {
|
||||
// 嘗試匹配現有的 NO-BATCH 紀錄
|
||||
const existingNoBatch = (batchesCache[item.productId]?.batches || []).find(b => b.batchNumber === 'NO-BATCH');
|
||||
handleUpdateItem(item.tempId, {
|
||||
batchMode: 'none',
|
||||
inventoryId: existingNoBatch?.inventoryId || undefined,
|
||||
originCountry: 'TW',
|
||||
expiryDate: undefined
|
||||
});
|
||||
} else {
|
||||
const selectedBatch = (batchesCache[item.productId]?.batches || []).find(b => b.inventoryId === value);
|
||||
handleUpdateItem(item.tempId, {
|
||||
@@ -568,9 +577,10 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ label: "📦 不使用批號 (自動累加)", value: "no_batch" },
|
||||
{ label: "+ 建立新批號", value: "new_batch" },
|
||||
...(batchesCache[item.productId]?.batches || []).map(b => ({
|
||||
label: `${b.batchNumber} - 庫存: ${b.quantity}`,
|
||||
label: `${b.batchNumber === 'NO-BATCH' ? '(無批號紀錄)' : b.batchNumber} - 庫存: ${b.quantity}`,
|
||||
value: b.inventoryId
|
||||
}))
|
||||
]}
|
||||
@@ -582,50 +592,55 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
{errors[`item-${index}-batch`]}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{item.batchMode === 'new' && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={item.originCountry || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.toUpperCase().slice(0, 2);
|
||||
handleUpdateItem(item.tempId, { originCountry: val });
|
||||
}}
|
||||
maxLength={2}
|
||||
placeholder="產地"
|
||||
className="h-8 text-xs text-center border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[3] text-xs bg-primary-50/50 text-primary-main px-2 py-1 rounded border border-primary-200/50 font-mono overflow-hidden whitespace-nowrap">
|
||||
{getBatchPreview(item.productId, product?.code, item.originCountry || 'TW', inboundDate)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={item.originCountry || ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.toUpperCase().slice(0, 2);
|
||||
handleUpdateItem(item.tempId, { originCountry: val });
|
||||
}}
|
||||
maxLength={2}
|
||||
placeholder="產地"
|
||||
className="h-8 text-xs text-center border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
{/* 新增效期輸入 (在新增批號模式下) */}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">效期:</span>
|
||||
<div className="relative flex-1">
|
||||
<Calendar className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={item.expiryDate || ""}
|
||||
onChange={(e) =>
|
||||
handleUpdateItem(item.tempId, {
|
||||
expiryDate: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 pl-8 text-xs border-gray-300 w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-[3] text-xs bg-primary-50/50 text-primary-main px-2 py-1 rounded border border-primary-200/50 font-mono overflow-hidden whitespace-nowrap">
|
||||
{getBatchPreview(item.productId, product?.code, item.originCountry || 'TW', inboundDate)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{/* 新增效期輸入 (僅在建立新批號模式下) */}
|
||||
{item.batchMode === 'new' && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">效期:</span>
|
||||
<div className="relative flex-1">
|
||||
<Calendar className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={item.expiryDate || ""}
|
||||
onChange={(e) =>
|
||||
handleUpdateItem(item.tempId, {
|
||||
expiryDate: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-8 pl-8 text-xs border-gray-300 w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.batchMode === 'none' && (
|
||||
<div className="mt-1 px-2 py-1 bg-amber-50 text-amber-700 text-[10px] rounded border border-amber-100 flex items-center gap-1">
|
||||
<span className="shrink-0 font-bold">INFO</span>
|
||||
系統將自動累加至該商品的通用庫存紀錄
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.batchMode === 'existing' && item.inventoryId && (
|
||||
<div className="flex flax-col gap-1 mt-1">
|
||||
<div className="text-xs text-gray-500 font-mono">
|
||||
效期: {item.expiryDate || '未設定'}
|
||||
效期: {item.expiryDate || '無效期紀錄'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -174,7 +174,7 @@ export interface InboundItem {
|
||||
largeUnit?: string;
|
||||
conversionRate?: number;
|
||||
selectedUnit?: 'base' | 'large';
|
||||
batchMode?: 'existing' | 'new'; // 批號模式
|
||||
batchMode?: 'existing' | 'new' | 'none'; // 批號模式
|
||||
inventoryId?: string; // 選擇現有批號時的 ID
|
||||
batchNumber?: string;
|
||||
originCountry?: string; // 新增產地
|
||||
|
||||
Reference in New Issue
Block a user